drupal-tutorial-video
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseDrupal Tutorial Video
Drupal教程视频
Overview
概述
Records a narrated 1920x1080 MP4 that shows how to set up a Drupal module. The skill reads
the module to learn the real setup, writes a storyboard for approval, then records each step
as a beat (one narration sentence + its action) with a visible cursor and
character-by-character typing, generates ElevenLabs narration per beat, length-fits each beat
so the words stay in sync with the picture, overlays a caption bar, and concatenates the
beats into one video.
The recording happens inside the ddev web container on a virtual X display () at
1920x1080. Two tools split the work:
:99- agent-browser (the brain): navigate, read the accessibility tree, locate an element, return its on-screen box, wait for page state. Its CDP input is synthetic and invisible, so it never does the visible clicking.
- xdotool (the hands): move the real X11 pointer to that box in small steps for smooth
motion, click, and type with a per-key delay. Because the pointer on is real,
:99captures it natively. What moves is exactly what clicks.ffmpeg -f x11grab
Terminal commands that cannot be shown in the browser (, ,
) are rendered as still command cards, never as a recorded terminal.
composer requiredrush enddev ...录制带有旁白的1920x1080分辨率MP4视频,展示Drupal模块的搭建流程。该技能会先读取模块代码以了解实际搭建步骤,编写分镜脚本待确认,然后将每个步骤作为一个**节拍(beat)**录制(一句旁白对应一个操作),包含可见光标和逐字符打字效果,为每个节拍生成ElevenLabs语音旁白,调整每个节拍的时长使旁白与画面保持同步,添加字幕栏,最后将所有节拍拼接为完整视频。
录制操作在ddev的web容器内的虚拟X显示器()上以1920x1080分辨率进行,由两个工具分工完成:
:99- agent-browser(核心控制工具):负责页面导航、读取无障碍树、定位元素、返回元素在屏幕上的坐标区域、等待页面状态加载完成。它通过CDP(Chrome DevTools Protocol)输入的操作是模拟且不可见的,因此不会产生可见的点击动作。
- xdotool(操作执行工具):将真实的X11指针逐步移动到目标坐标区域以实现平滑移动,执行点击操作,并添加按键延迟模拟真实打字。由于虚拟显示器上的指针是真实存在的,
:99可以原生捕获到它的动作,移动的指针就是实际执行点击的指针。ffmpeg -f x11grab
无法在浏览器中展示的终端命令(如、、)会生成为静态命令卡片,而非录制终端画面。
composer requiredrush enddev ...Requirements (hard)
硬性要求
- A running ddev project. You need its site URL, an admin username, and a password.
- agent-browser in the web container (preflight installs it there with ; it drives Chromium over CDP on the container's localhost). See https://github.com/vercel-labs/agent-browser.
npm - awaz (, https://github.com/ahmadawais/awaz), an ElevenLabs TTS wrapper. Needs
npm i -g awazin the environment (NOTELEVENLABS_API_KEY, which has no TTS). The key must have the Text to Speech and Voices (read) permissions. TTS lives behind the@elevenlabs/clisubcommand:speak. The top-levelawaz speak --voice-id <id> --no-play -o out.mp3 "text"is-v(it prints the version, exits 0, and writes no file, a silent failure that only surfaces at--version), andfinish-beat.shis required headless or awaz tries to open a speaker device and fails. Store the key somewhere like--no-playand export it before recording; nothing sets it for you.~/.config/elevenlabs/key - ffmpeg, Xvfb, xdotool, chromium in the web container (preflight installs these).
- Montserrat TTF for the caption bar (preflight downloads it if missing).
Run first; it checks and sets up all of the above.
preflight.shContrib modules installed from a git source can block every . If a contrib
module sits on a local branch with unpushed commits, composer refuses any require (not just ones
touching that package) because it wants to restore the locked ref:
. Non-destructive
workaround, after confirming the commit is genuinely local-only ( returns nothing) and taking a safety bundle:
composer requireSource directory .../contrib/<name> has unpushed changes on the current branchgit -C <dir> branch -r --contains <sha>git -C web/modules/contrib/<name> bundle create /backup/<name>.bundle --all
git -C web/modules/contrib/<name> checkout <locked ref from composer.lock>The branch still exists on disk and can be checked out again afterwards.
- 运行中的ddev项目,需获取站点URL、管理员用户名和密码。
- web容器中安装有agent-browser(预检查脚本会通过
preflight.sh在容器内安装;它通过容器本地的CDP驱动Chromium)。详情见https://github.com/vercel-labs/agent-browser。npm - awaz(执行安装,项目地址:https://github.com/ahmadawais/awaz),ElevenLabs的TTS包装工具。需要在环境变量中配置`ELEVENLABS_API_KEY`(注意不是`@elevenlabs/cli`,该工具无TTS功能)。API密钥必须拥有**文本转语音**和**语音读取**权限。TTS功能通过`speak`子命令调用:`awaz speak --voice-id <id> --no-play -o out.mp3 "text"
npm i -g awaz-v。顶层参数--version代表finish-beat.sh(仅打印版本信息,退出码为0,且不会生成任何文件,这个静默失败只会在--no-play中暴露);~/.config/elevenlabs/key`这类位置,录制前导出该环境变量;工具不会自动设置密钥。是无头环境下的必填参数,否则awaz会尝试打开音频设备并失败。建议将密钥存储在 - web容器中安装有ffmpeg、Xvfb、xdotool、chromium(预检查脚本会安装这些工具)。
- 字幕栏所需的Montserrat TTF字体(预检查脚本会在缺失时自动下载)。
请先运行;它会检查并配置上述所有依赖项。
preflight.sh从Git源安装的贡献模块可能会阻止所有操作。如果某个贡献模块处于本地分支且存在未推送的提交,composer会拒绝任何操作(不仅限于涉及该包的操作),原因是它希望恢复锁定的引用:。在确认提交仅存在于本地(执行无返回结果)并做好安全备份后,可采用非破坏性解决方案:
composer requirerequireSource directory .../contrib/<name> has unpushed changes on the current branchgit -C <dir> branch -r --contains <sha>git -C web/modules/contrib/<name> bundle create /backup/<name>.bundle --all
git -C web/modules/contrib/<name> checkout <locked ref from composer.lock>本地分支仍会保留在磁盘上,后续可再次切换回去。
Helper scripts
辅助脚本
Run every script from the ddev project root, with set
(a short kebab-case name for this tutorial, e.g. ). Scripts read
for shared paths and settings.
export TUT_SLUG=<tutorial-slug>commerce-checkoutlib.sh| Script | Runs on | Purpose |
|---|---|---|
| host | Check + set up ddev, container packages, agent-browser, awaz, Montserrat; create the build dir and copy the container helpers |
| host | Bring up Xvfb |
| host | Start/stop the x11grab capture for beat |
| host | Record a concept/intro slide beat (local HTML at |
| host | The browser-action driver: find an element ( |
| container | The raw cursor and typing (called by |
| host | Render a command card into |
| host | Trim an over-long capture, keeping |
| host | Sanity-check one beat: duration, a still frame, stub/overlong flags |
| container | Audit the whole set at once: flag |
| container | Fade the abrupt tail of every narration file and pad real silence (before finishing) |
| container | Cut trailing frozen tails ( |
| host | Pad video to the narration, add lead/tail silence, mux audio, draw the caption bar |
| host | Run |
| host | Concatenate |
Container helpers (, , , ) are copied into the build
dir by preflight and run inside the container, e.g.
(this container path is in
, used as shorthand below). They loop over 90+ files in a
single script file on purpose: an inline loop breaks, because the
host shell expands before the container ever sees it (see "Getting commands past ").
hands.shaudit.shfade-audio.shdeadair.shddev exec bash /var/www/html/.tutorial-build/<slug>/audit.sh$CDIRlib.shddev exec bash -lc "for ...; do"$varddev exec所有脚本需在ddev项目根目录运行,且需先设置(为教程设置一个短横线命名的别名,例如)。脚本会读取中的共享路径和配置。
export TUT_SLUG=<tutorial-slug>commerce-checkoutlib.sh| 脚本 | 运行环境 | 用途 |
|---|---|---|
| 主机 | 检查并配置ddev、容器包、agent-browser、awaz、Montserrat字体;创建构建目录并复制容器辅助脚本 |
| 主机 | 启动Xvfb |
| 主机 | 启动/停止节拍 |
| 主机 | 录制概念/介绍类幻灯片节拍(本地HTML路径为 |
| 主机 | 浏览器操作驱动工具:查找元素( |
| 容器 | 原生光标和打字操作工具(由 |
| 主机 | 将命令生成为命令卡片并保存到 |
| 主机 | 裁剪过长的捕获内容,保留原始文件 |
| 主机 | 对单个节拍进行 sanity 检查:时长、静态帧、是否为无效/过长片段 |
| 容器 | 批量检查所有节拍:标记 |
| 容器 | 为每个旁白文件的结尾添加淡入淡出效果,并补充真实静音片段(在完成处理前) |
| 容器 | 裁剪末尾的静止画面(使用 |
| 主机 | 将视频时长匹配旁白时长,添加开头/结尾静音,混流音频,绘制字幕栏 |
| 主机 | 对所有节拍执行 |
| 主机 | 将 |
容器辅助脚本(、、、)会被预检查脚本复制到构建目录,在容器内运行,例如(该容器路径在中定义为,以下用该别名指代)。这些脚本将90多个文件的循环逻辑整合到单个脚本中是有意为之:如果使用内联循环,主机shell会在容器执行前先展开变量,导致循环失效(详见「让命令通过正常执行」)。
hands.shaudit.shfade-audio.shdeadair.shddev exec bash /var/www/html/.tutorial-build/<slug>/audit.shlib.sh$CDIRddev exec bash -lc "for ...; do"$varddev execBuild directory
构建目录
Everything lives under the ddev mount so host and container share one filesystem:
<project>/.tutorial-build/<slug>/
storyboard.md
scene-final.txt # optional: beat numbers that end a scene (finish-all.sh reads it)
hands.sh # container helpers, copied here by preflight so the container can run them
audit.sh fade-audio.sh deadair.sh
assets/Montserrat-*.ttf
slides/NN.html # local concept/intro slides (served to the kiosk browser)
beats/NN.mp4 # raw silent capture for beat NN (or command card)
beats/NN.orig.mp4 # untouched capture kept by trim.sh
beats/NN.pretrim.mp4 # untouched capture kept by deadair.sh
audio/NN.mp3 # narration for beat NN, from awaz (faded)
audio/NN.orig.mp3 # untouched narration kept by fade-audio.sh
final/NN.caption.txt # caption bar text for beat NN (one line)
final/beat-NN.mp4 # padded + muxed + captioned
final/tutorial.mp4 # concatenated resultNothing is deleted at the end. The user may ask for changes. The / copies mean any
trim or fade can be redone without re-recording.
.orig.pretrim所有文件都存储在ddev挂载目录下,以便主机和容器共享同一个文件系统:
<project>/.tutorial-build/<slug>/
storyboard.md
scene-final.txt # 可选:标记场景结尾的节拍编号(finish-all.sh会读取该文件)
hands.sh # 容器辅助脚本,由预检查脚本复制到此处供容器运行
audit.sh fade-audio.sh deadair.sh
assets/Montserrat-*.ttf
slides/NN.html # 本地概念/介绍类幻灯片(供kiosk浏览器加载)
beats/NN.mp4 # 节拍NN的原始静音捕获文件(或命令卡片)
beats/NN.orig.mp4 # trim.sh保留的未修改捕获文件
beats/NN.pretrim.mp4 # deadair.sh保留的未修改捕获文件
audio/NN.mp3 # 节拍NN的旁白文件,由awaz生成(已添加淡入淡出)
audio/NN.orig.mp3 # fade-audio.sh保留的未修改旁白文件
final/NN.caption.txt # 节拍NN的字幕栏文本(单行)
final/beat-NN.mp4 # 已调整时长、混流音频、添加字幕的节拍文件
final/tutorial.mp4 # 拼接后的最终视频处理结束后不会删除任何文件,因为用户可能会提出修改需求。/备份文件意味着无需重新录制即可重新执行裁剪或淡入淡出操作。
.orig.pretrimScenes and beats
场景与节拍
A beat is the atomic unit: one narration sentence and the single action it describes,
recorded as its own clip. A scene is just a storyboard grouping of consecutive beats
(a heading like "Configure the provider"); it has no separate file.
Beats are the reason narration stays in sync with the picture. Each beat's video and its
narration are the same clip, so the words cannot drift from the action:
length-fits each beat to , and the beats concatenate in order. Do not
record a whole scene as one take with one long narration; that is what makes audio and video
drift.
finish-beat.shmax(action, speech)Beats are numbered globally, , , , ... in play order. The scene grouping lives
only in for human organization.
010203storyboard.mdThe beat number is the edit timeline; leave gaps. orders by a plain filename sort
of , so the number is the play order. Two consequences:
concat.shfinal/beat-*.mp4- There is no room to insert. Adding a beat between 89 and 90 means renumbering, and a
scheme is unsafe (plain
beat-89bis locale-collated and may ignore punctuation). Reordering requests arrive after everything is recorded, so number in steps of 10 (sort,010,020, ...) from the start; insertion then costs nothing. When a late edit needs a new beat mid-sequence and you did not leave gaps, the cheap move is to swap two adjacent beats whose content can trade places (two slides, say).030 - Removing beats is free. Gaps concatenate fine; cut a beat's file and nothing else changes.
Derive the scene-final list, never hardcode it. gives the last beat of each
scene a longer tail. Feed it the list from (or a with
), produced from the storyboard, so reordering cannot silently leave the pause on
the wrong beat.
finish-all.shscene-final.txtbeats.jsonscene_final: true**节拍(beat)**是最小单元:对应一句旁白和它描述的单个操作,作为独立片段录制。**场景(scene)**只是分镜脚本中对连续节拍的分组(例如标题为「配置提供商」),没有单独的文件。
节拍是确保旁白与画面同步的关键。每个节拍的视频和旁白是同一个片段,因此旁白不会与画面脱节:会将每个节拍的时长调整为,然后按顺序拼接所有节拍。请勿将整个场景作为一个长片段录制并搭配一段长旁白,这会导致音画不同步。
finish-beat.shmax(操作时长, 旁白时长)节拍按播放顺序全局编号,例如、、... 场景分组仅在中存在,用于人工整理。
010203storyboard.md节拍编号即编辑时间线,请预留间隙。会按的文件名排序来确定播放顺序,因此编号直接决定播放顺序。这会带来两个影响:
concat.shfinal/beat-*.mp4- 无法插入新节拍。如果要在89和90之间插入节拍,需要重新编号,而这类命名方式不安全(普通排序是基于区域设置的,可能会忽略标点符号)。修改顺序的需求通常在录制完成后提出,因此从一开始就以10为步长编号(
beat-89b、010、020...);这样插入新节拍无需任何额外操作。如果后期编辑需要在序列中间添加新节拍但未预留间隙,简便方法是交换两个内容可互换的相邻节拍(例如两张幻灯片)。030 - 删除节拍无需额外操作。编号间隙不会影响拼接;只需删除对应节拍的文件即可,其他内容无需修改。
自动推导场景结尾列表,不要硬编码。会为每个场景的最后一个节拍添加更长的结尾留白。请从分镜脚本生成的(或带有的)中读取该列表,这样即使调整节拍顺序,也不会错误地将留白添加到错误的节拍上。
finish-all.shscene-final.txtscene_final: truebeats.jsonBeat taxonomy
节拍分类
| Type | Shows | Produced by |
|---|---|---|
| The module's drupal.org project page ( | browser-record |
| The module page as reference | browser-record |
| A real setup step in the ddev site, visible cursor and typing | agent-browser + hands.sh + record-beat.sh |
| A terminal command that cannot be shown in-browser | make-card.sh |
| Recap / call to action | browser-record or command-card |
Intro caveat: the kiosk Chromium is served HTTP 406 by (looks like TLS or
client-hint fingerprinting: from the same container with Chrome-like headers gets 200, the
browser with a normal UA does not, and UA / / flags do not fix it).
So the and types that open may not be
recordable here. If fails with , fall back to
a locally rendered slide () that lists the project and why it matters, keep the
project URL in the caption, and tell the user why.
drupal.orgcurlChrome/*Accept*--langintromodule-pagedrupal.org/project/<machine_name>agent-browser openERR_HTTP_RESPONSE_CODE_FAILURErecord-slide.shSlides are cheap: machine-generated HTML slides beat screenshots for concept beats (regenerating all
34 after a "3 lanes -> 5 lanes" content change was one script run). Text-to-image is fine for
backdrops (a hero image at behind real HTML type) but useless for text - render the
type as HTML, and leave the bottom ~15% empty for the caption bar.
opacity:.42| 类型 | 展示内容 | 生成方式 |
|---|---|---|
| Drupal模块的drupal.org项目页面( | 浏览器录制 |
| 模块页面作为参考 | 浏览器录制 |
| ddev站点中的真实搭建步骤,包含可见光标和打字效果 | agent-browser + hands.sh + record-beat.sh |
| 无法在浏览器中展示的终端命令 | make-card.sh |
| 内容回顾/行动号召 | 浏览器录制或命令卡片 |
介绍类节拍注意事项:kiosk模式的Chromium访问时会收到HTTP 406响应(看起来是TLS或客户端提示指纹识别问题:从同一容器使用类Chrome头信息执行可得到200响应,但使用正常 UA的浏览器无法访问,且修改UA、、参数均无法解决)。因此,打开的和类型节拍可能无法录制。如果失败并提示,请改用本地渲染的幻灯片(),列出项目信息和重要性,在字幕中保留项目URL,并告知用户原因。
drupal.orgcurlChrome/*Accept*--langdrupal.org/project/<machine_name>intromodule-pageagent-browser openERR_HTTP_RESPONSE_CODE_FAILURErecord-slide.sh幻灯片制作成本低:机器生成的HTML幻灯片比截图更适合概念类节拍(当内容从「3个步骤」改为「5个步骤」时,重新生成34张幻灯片只需运行一次脚本)。文本转图像适合作为背景(例如不透明度为0.42的英雄图像叠加在真实HTML文本上),但不适合文本内容——请用HTML渲染文本,并为字幕栏预留底部约15%的空白区域。
Getting commands past ddev exec
ddev exec让命令通过ddev exec
正常执行
ddev execAlmost every hard bug in this skill traces to one fact: (and the helper)
re-parse their arguments through an extra shell before the container sees them. Anything with
quotes, braces, backslashes, , , or commas is mangled, and it almost always fails
silently rather than erroring. Real failures from one run:
ddev execcexec$(...)$var- A wait loop with inside
$(seq ...)killed the container shell before itscexecran, so captures never stopped and later beats came out as 48-byte stubs.pkill - Typing the regex produced
/[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/ion screen: brace expansion ate...+.[a-z]2/iand{2,}lost its backslash. The beat looked fine but the pattern could not match an email, so the guardrail being demonstrated would not have worked.\. - lost the quotes, evaluated
agent-browser eval "location.hash='pre'"(named-element access tolocation.hash=pre), and silently set the hash to garbage. Three beats recorded the wrong state; the only tell was byte-identical<div id="pre">files..mp4
Rules:
- Keep each container command to a single simple statement. Do all control flow (loops, waits,
retries) on the host, one plain per iteration.
cexec - Type through : base64 on the host (
hands.sh type64), decode past the boundary in the container. Never use the plainprintf %s 'text' | base64 -w0path for anything but bare ASCII words.type - For , wrap the whole call in
agent-browser evalwith escaped double quotes and use single quotes only inside the JS expression, or avoidbash -lcand navigate with a hash URL.eval - Loops break even inside .
bash -lcfails withddev exec bash -lc "for t in 1 2 3; do ffmpeg -ss $t ...; done"because the host shell expandst: unbound variableto empty first. Either loop on the host (one$tper iteration) or put the loop in a script file and runddev exec(much faster for 90+ iterations; this is why the container helpers are files, not inline loops).ddev exec bash /var/www/html/.../script.sh - with non-trivial quoting silently produces nothing. A one-liner iterating plugin definitions returned empty output at exit 0; the same code as a file via
drush evworked. Preferdrush php:script foo.phpfor anything beyond a bare expression.php:script
该技能中几乎所有棘手的bug都源于一个事实:(以及辅助工具)会在容器执行前通过额外的shell重新解析参数。任何包含引号、大括号、反斜杠、、或逗号的内容都会被破坏,且几乎总是静默失败而非报错。以下是实际运行中遇到的失败案例:
ddev execcexec$(...)$var- 中的
cexec等待循环在$(seq ...)执行前杀死了容器shell,导致捕获无法停止,后续节拍生成的是48字节的无效文件。pkill - 输入正则表达式时,屏幕上显示的是
/[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/i:大括号扩展吃掉了...+.[a-z]2/i,{2,}丢失了反斜杠。节拍看起来正常,但该模式无法匹配邮箱,导致演示的防护机制无法正常工作。\. - 丢失了引号,实际执行的是
agent-browser eval "location.hash='pre'"(访问location.hash=pre的命名元素),静默地将哈希值设置为无效内容。录制了三个节拍的错误状态;唯一的线索是<div id="pre">文件字节完全相同。.mp4
规则:
- 每个容器命令保持为单个简单语句。所有控制流(循环、等待、重试)在主机上执行,每次迭代调用一次普通的。
cexec - 通过输入文本:在主机上进行base64编码(
hands.sh type64),在容器内解码后传递。除非是纯ASCII单词,否则不要使用普通的printf %s 'text' | base64 -w0方式。type - 对于,将整个调用包裹在带有转义双引号的
agent-browser eval中,JS表达式内部仅使用单引号,或者避免使用bash -lc,改用哈希URL导航。eval - 即使在中,循环也会失效。
bash -lc会因ddev exec bash -lc "for t in 1 2 3; do ffmpeg -ss $t ...; done"失败,因为主机shell会先将t: unbound variable展开为空。要么在主机上循环(每次迭代调用一次$t),要么将循环逻辑写入脚本文件,然后执行ddev exec(对于90+次迭代,这种方式快得多;这也是容器辅助脚本采用文件形式而非内联循环的原因)。ddev exec bash /var/www/html/.../script.sh - 带有复杂引号的会静默地无输出。一个遍历插件定义的单行命令返回空输出且退出码为0;将相同代码写入文件并通过
drush ev执行则正常工作。除了简单表达式外,优先使用drush php:script foo.php。php:script
Workflow
工作流程
Create a todo per step.
-
Preflight.then
export TUT_SLUG=<slug>. Confirm the ddev URL, username, and password with the user../skills/.../preflight.sh -
Ask the intro question. Ask whether the video should open with a why-this-matters / marketing intro over the module's drupal.org page, or go straight to the steps.
-
Read the module first, and map the whole followable path. Read, README, config forms, permissions, routing, and services to derive the actual setup steps from the code, not from assumptions. Same "read first" discipline as
<machine_name>.info.yml. Two things a "technically correct" tutorial still gets wrong:drupal-module-documentation- Module-set completeness. A tutorial you cannot follow is a defect. List every route the
later scenes depend on and check each resolves with the set you install. Admin UIs often live
in a separate submodule (ECA needs for
eca_ui; installing/admin/config/workflow/eca+eca+eca_contentalone leaves the viewer with no way in). One module filter can sometimes cover a whole set when the descriptions cross-reference each other.eca_tool - Confirm a "missing" feature from a second angle before acting on it. A throwaway probe once
reported zero action plugins and nearly got a working module written off; they existed the
whole time. In plugin-land, is a cheap oracle: its exception enumerates every valid id.
getDefinition('<known-bad-id>')
- Module-set completeness. A tutorial you cannot follow is a defect. List every route the
later scenes depend on and check each resolves with the set you install. Admin UIs often live
in a separate submodule (ECA needs
-
Write the storyboard. Writeas scenes (headings) broken into beats. Each beat is one narration sentence and the single action it describes, and carries: the global beat number,
.tutorial-build/<slug>/storyboard.md, the on-screen action(s), the narration sentence, and the one-line caption. For any beat that navigates to or acts on a route, put the admin path or link in that caption (see Caption bar). Keep beats to one sentence + one action so the narration cannot drift. Get the user's approval before recording.type -
Pick a voice. Run, present the options with their names and ids, and ask the user which voice to use. Remember the chosen voice id for
awaz voices.--voice-id -
Start the session.. This opens the kiosk browser on
./session.sh start "<site-url>"inside the container.:99 -
Record beat by beat. For each beat, in order:
- Write the caption to (one line, plain).
final/NN.caption.txt - browser-action:
./record-beat.sh start NN- Drive the browser with , which resolves the element, scrolls it into the safe viewport band, and moves + clicks with the visible cursor (all the
ui.shquoting lives in one place; see "Finding and clicking elements"):ddev execPick the matcher for the target:./ui.sh open "<url>" ./ui.sh click name "modules[<machine_name>][enable]" # find -> scroll into view -> move + click ./ui.sh type "a value to type" # metacharacter-safe (type64) ./ui.sh click nth "op:1" # the 2nd name="op" button, e.g. Savefor form fields,namefor same-named submit buttons,nth/linkfor admin links and React rows. Each beat is one action, so keep it short. Before typing into a field that may already hold text (search/filter fields keep their value across reloads), clear it first (anythen./ui.sh key ctrl+a). Pace it like a human. Leave the result on screen for a moment before stopping.type ./record-beat.sh stop NN
- slide (concept / intro / outro): . Local HTML is the practical answer for concept beats and for anything that would open
TUT_SLIDES_URL=<base> ./record-slide.sh NN(which 406s the kiosk browser). Keep the project URL in the caption. Leave the bottom ~15% of each slide empty for the caption bar.drupal.org - command-card: .
./make-card.sh NN 4 "composer require drupal/<name>"
Beats that share one page load must be recorded as one continuous sequence. If beats 33-35 depend on checkbox state persisting across a client-side filter change, re-recording one in isolation loses that state. - Write the caption to
-
Generate narration. For each beat, generate its one-sentence voice-over with thesubcommand (the top-level
speakis-vand writes no file;--versionis required headless):--no-playawaz speak --voice-id <voice-id> --no-play -o .tutorial-build/<slug>/audio/NN.mp3 "<beat narration sentence>"Optional flags:,--speed 0.5-2.0,--stability 0-1,--style 0-1.--model-id <id> -
Post-production on the raw material (before finishing). Two passes must run beforemuxes and pads (see "Post-production: dead air and audio"):
finish-beat.sh- Fade narration tails: . ElevenLabs ends each line mid-sound, so unfaded audio clips audibly against the padded silence.
ddev exec bash $CDIR/fade-audio.sh - Trim trailing dead air: to see which beats run long, then
ddev exec bash $CDIR/audit.shandddev exec bash $CDIR/deadair.sh dry(it verifies each cut and reverts bad ones). Use... applyfor beats deadair leaves alone (a blinking cursor never reads as frozen)../trim.sh NN head|tail <secs>
- Fade narration tails:
-
Finish the beats.runs
./finish-all.shover every beat and gives scene-final beats a longer tail (fromfinish-beat.sh). Per beat it length-fits toscene-final.txt, adds lead/tail silence, freeze-pads the video so it never ends before the narration, muxes the audio, and draws the caption bar. (One beat:max(action, speech), or./finish-beat.sh NNfor a scene-final pause.)TUT_TAIL=1.5 ./finish-beat.sh NN -
Verify the whole set, not just that files exist. Five separate failures in one run left beat files that existed at non-zero size but showed the wrong state. Runon anything suspect (duration + a still frame + stub/overlong flags) and
./check-beat.sh NNfor a one-pass pacing sweep. Identical file size to the previous beat usually means nothing changed on screen. And after any form-submit beat, read the state back instead of eyeballing the video:audit.shfor config (prefer specific keys over scanning YAML),drush config:get <id>for module state,drush pmlfor content. This is the strongest check that the on-camera action landed.drush sqlq -
Concatenate and present.(the final encode of a long tutorial takes several minutes; it refuses to start if a prior encode is still running in the container), then show the user
./concat.sh. Do not clean up. Wait for change requests; re-record or re-finish only the affected beats and re-run.tutorial-build/<slug>/final/tutorial.mp4.concat.sh
为每个步骤创建待办事项。
-
预检查。设置,然后运行
export TUT_SLUG=<slug>。与用户确认ddev URL、用户名和密码。./skills/.../preflight.sh -
询问介绍类内容需求。询问用户视频是否需要以模块drupal.org页面为背景,添加关于模块重要性/推广的介绍类旁白,还是直接进入搭建步骤。
-
先读取模块代码,梳理完整的可跟随路径。读取、README、配置表单、权限、路由和服务文件,从代码中推导实际搭建步骤,而非基于假设。遵循与
<machine_name>.info.yml相同的「先读取代码」原则。即使是「技术上正确」的教程也可能犯以下两类错误:drupal-module-documentation- 模块集完整性。无法跟随的教程是缺陷。列出后续场景依赖的所有路由,并检查每个路由在已安装的模块集中是否可访问。管理UI通常存在于单独的子模块中(例如ECA需要才能访问
eca_ui;仅安装/admin/config/workflow/eca+eca+eca_content会导致用户无法进入管理界面)。有时一个模块过滤器可以覆盖整个模块集,因为它们的描述会相互引用。eca_tool - 从另一个角度确认「缺失」的功能后再行动。一次随意的探测曾报告没有动作插件,差点导致一个可用的模块被判定为无效;实际上这些插件一直存在。在插件体系中,是一个简单有效的验证方式:它抛出的异常会列出所有有效的ID。
getDefinition('<known-bad-id>')
- 模块集完整性。无法跟随的教程是缺陷。列出后续场景依赖的所有路由,并检查每个路由在已安装的模块集中是否可访问。管理UI通常存在于单独的子模块中(例如ECA需要
-
编写分镜脚本。在中编写场景(标题),并拆分为节拍。每个节拍对应一句旁白和它描述的单个操作,包含:全局节拍编号、类型、屏幕操作、旁白语句和单行字幕。对于任何导航到或操作某个路由的节拍,将管理路径或链接添加到字幕中(见「字幕栏」部分)。每个节拍保持为一句旁白+一个操作,避免旁白与画面脱节。录制前需获得用户确认。
.tutorial-build/<slug>/storyboard.md -
选择语音。运行,展示语音选项的名称和ID,询问用户选择哪一种语音。记录所选的语音ID,用于
awaz voices参数。--voice-id -
启动会话。运行,这会在容器内的
./session.sh start "<site-url>"显示器上打开kiosk模式的浏览器。:99 -
逐个录制节拍。按顺序录制每个节拍:
- 将字幕写入(单行纯文本)。
final/NN.caption.txt - 浏览器操作类节拍:
- 运行
./record-beat.sh start NN - 使用****驱动浏览器,它会定位元素、滚动到安全视口区域、移动并点击可见光标(所有
ui.sh的引号处理都集中在一处;见「定位和点击元素」):ddev exec根据目标选择匹配器:./ui.sh open "<url>" ./ui.sh click name "modules[<machine_name>][enable]" # 定位 -> 滚动到可见区域 -> 移动并点击 ./ui.sh type "a value to type" # 支持特殊字符(使用type64) ./ui.sh click nth "op:1" # 第二个name="op"按钮,例如「保存」用于表单字段,name用于同名提交按钮,nth/link用于管理链接和React行。每个节拍对应一个操作,保持简短。在可能已有文本的字段中输入前(搜索/过滤字段会在重载后保留值),先清空字段(any然后输入)。以人类的节奏操作,停止录制前让结果在屏幕上停留片刻。./ui.sh key ctrl+a - 运行
./record-beat.sh stop NN
- 运行
- 幻灯片类节拍(概念/介绍/结尾):运行。本地HTML是概念类节拍和所有需打开
TUT_SLIDES_URL=<base> ./record-slide.sh NN(会返回406)的节拍的实用解决方案。在字幕中保留项目URL。为字幕栏预留每张幻灯片底部约15%的空白区域。drupal.org - 命令卡片类节拍:运行。
./make-card.sh NN 4 "composer require drupal/<name>"
共享同一页面加载状态的节拍必须连续录制。如果节拍33-35依赖复选框状态在客户端过滤更改后保持不变,单独重新录制其中一个节拍会丢失该状态。 - 将字幕写入
-
生成旁白。为每个节拍使用子命令生成单句语音旁白(顶层参数
speak是-v,不会生成文件;--version是无头环境下的必填参数):--no-playawaz speak --voice-id <voice-id> --no-play -o .tutorial-build/<slug>/audio/NN.mp3 "<beat narration sentence>"可选参数:、--speed 0.5-2.0、--stability 0-1、--style 0-1。--model-id <id> -
原始素材后期处理(在完成节拍处理前)。在混流和调整时长前,必须执行以下两个步骤(见「后期处理:静音片段和音频」):
finish-beat.sh- 旁白结尾淡入淡出:运行。ElevenLabs生成的旁白结尾没有衰减,未处理的音频片段与添加的静音片段拼接时会出现明显的剪辑痕迹。
ddev exec bash $CDIR/fade-audio.sh - 裁剪末尾静音片段:运行查看哪些节拍时长过长,然后运行
ddev exec bash $CDIR/audit.sh和ddev exec bash $CDIR/deadair.sh dry(它会验证每个裁剪操作,恢复错误裁剪)。对于deadair未处理的节拍(例如闪烁的光标不会被判定为静止),使用... apply手动裁剪。./trim.sh NN head|tail <secs>
- 旁白结尾淡入淡出:运行
-
完成节拍处理。运行对所有节拍执行
./finish-all.sh,并为场景结尾的节拍添加更长的结尾留白(读取finish-beat.sh)。对于每个节拍,它会将时长调整为scene-final.txt,添加开头/结尾静音,冻结画面使视频不会早于旁白结束,混流音频,并绘制字幕栏。(单个节拍处理:运行max(操作时长, 旁白时长),或运行./finish-beat.sh NN为场景结尾节拍设置更长的留白。)TUT_TAIL=1.5 ./finish-beat.sh NN -
验证整个视频集,不要仅检查文件是否存在。一次运行中出现的五个独立故障导致节拍文件存在且大小非零,但展示的是错误状态。对可疑的节拍运行(检查时长+静态帧+无效/过长标记),运行
./check-beat.sh NN进行一次批量节奏检查。与前一个节拍文件大小相同通常意味着屏幕上没有发生任何变化。在任何表单提交节拍后,读取返回的状态而非仅查看视频:使用audit.sh检查配置(优先查看特定键而非扫描YAML),drush config:get <id>检查模块状态,drush pml检查内容。这是验证屏幕操作是否生效的最可靠方式。drush sqlq -
拼接并交付。运行(长教程的最终编码需要几分钟;如果容器内已有编码进程在运行,脚本会拒绝启动),然后向用户展示
./concat.sh。不要清理文件。等待用户的修改请求;仅重新录制或重新处理受影响的节拍,然后重新运行.tutorial-build/<slug>/final/tutorial.mp4。concat.sh
Caption bar
字幕栏
finish-beat.shdrawboxblack@0.94final/NN.caption.txtexpansion=noneblack@0.850.94expansion=nonedrawtext%{...}textfile%Show the path or link in the caption. When a beat navigates somewhere or acts on a
specific route, put the admin path (or URL) in the caption so a viewer can follow along
without pausing. Use the route the user actually types or clicks, not the narration
restated:
- Navigating to a config page:
Configuration > System > Site information (/admin/config/system/site-information) - Clicking a menu link or tab:
Manage > Extend (/admin/modules) - A command-card beat: show the command itself, e.g. .
composer require drupal/<name>
Keep it to one line: if the breadcrumb plus path is too long, show just the path
(). Paths are literal, so they are exempt from the
prose style rules (a real path may contain characters the style section otherwise avoids).
/admin/config/system/site-informationfinish-beat.shdrawboxblack@0.94final/NN.caption.txtexpansion=noneblack@0.850.94expansion=nonedrawtexttextfile%{...}%在字幕中显示路径或链接。当节拍导航到某个位置或操作特定路由时,将管理路径(或URL)添加到字幕中,以便观众无需暂停即可跟随操作。使用用户实际输入或点击的路由,而非旁白的复述:
- 导航到配置页面:
Configuration > System > Site information (/admin/config/system/site-information) - 点击菜单链接或标签:
Manage > Extend (/admin/modules) - 命令卡片类节拍:直接显示命令,例如。
composer require drupal/<name>
保持为单行:如果面包屑加路径过长,仅显示路径()。路径是字面内容,因此不受散文风格规则限制(实际路径可能包含风格规则中避免的字符)。
/admin/config/system/site-informationNarration writing style
旁白写作风格
The spoken narration and captions use the same voice as the other skills in this repo:
plain, direct, terse, active voice. No em dashes or en dashes. No marketing hype or
subjective qualifiers in the step narration (the opt-in intro may say why the module
matters, but still in verifiable terms). No emojis.
口语旁白和字幕使用与本仓库中其他技能相同的风格:简洁、直接、简短、主动语态。不要使用破折号。步骤旁白中不要包含营销噱头或主观限定词(可选的介绍类内容可以说明模块的重要性,但仍需使用可验证的表述)。不要使用表情符号。
Finding and clicking elements (ui.sh
)
ui.sh定位和点击元素(ui.sh
)
ui.shui.sh.mp4Off-viewport clicks fail silently. A click below y≈1000 or above y≈80 lands outside the
1080 kiosk viewport. reports success, the beat records normally, and the form simply never
submits (a Save button at y=1099, "below the fold"). scrolls the target to mid-screen and
re-measures before clicking. agent-browser has no negative scroll, so a target above the fold
needs .
xdotoolui.shscroll up NFour matcher kinds, not one:
| Kind | Use for |
|---|---|
| Form fields (the default). Survives |
| Same-named buttons: every Drupal submit is |
| Anchors by exact text. Beats hidden sidebar |
| React UIs: clickable rows are plain |
Plus (raw CSS) and (substring over curated tags). Substring matching is dangerous
on admin pages: matched "Blocks" in the sidebar and threw the cursor across the
screen. Prefer //exact for short words, or scope the search to a container.
seltexttext "Lock"linkanyReject invisible and zero-size elements. -hidden fields (an Authorization-prefix that
only appears once a key is chosen) return a box of ; moving there parks the cursor in the
top-left corner on camera. refuses and filters matchers on
so hidden duplicate controls do not win.
#states0,0ui.sh0,0getBoundingClientRect().width > 0Field names worth remembering:
- Node form title: (not
title[0][value])title - Module enable checkbox:
modules[<machine_name>][enable] - Module filter:
text
Verify with , not a post-hoc screenshot. An open dropdown, a hover
state, or a tooltip is gone by the time a screenshot runs; the recorded frame is the truth.
check-beat.sh<select>ui.sh.mp4视口外点击会静默失败。点击位置低于y≈1000或高于y≈80会落在1080分辨率的kiosk视口之外。会报告成功,节拍录制正常,但表单从未提交(例如位于y=1099的「保存」按钮,在「折叠下方」)。会将目标滚动到屏幕中间,并在点击前重新测量位置。agent-browser不支持向上滚动,因此位于折叠上方的目标需要执行。
xdotoolui.shscroll up N四种匹配器,而非一种:
| 类型 | 适用场景 |
|---|---|
| 表单字段(默认选项)。可在 |
| 同名按钮:每个Drupal提交按钮的name都是 |
| 按精确文本匹配锚点。避免匹配到侧边栏中隐藏的 |
| React UI:可点击的行是普通 |
此外还有(原生CSS选择器)和(在预设标签中进行子字符串匹配)。子字符串匹配在管理页面上很危险:会匹配到侧边栏中的**"Blocks"**,导致光标跳转到屏幕另一侧。对于短单词,优先使用//精确匹配,或限定搜索范围到某个容器。
seltexttext "Lock"linkany拒绝不可见和零大小的元素。隐藏的字段(例如仅在选择密钥后才显示的授权前缀)会返回的坐标;移动到该位置会使光标停留在屏幕左上角。会拒绝的坐标,并过滤掉的匹配器,避免隐藏的重复控件被选中。
#states0,0ui.sh0,0getBoundingClientRect().width > 0值得记住的字段名:
- 节点表单标题:(不是
title[0][value])title - 模块启用复选框:
modules[<machine_name>][enable] - 模块过滤器:
text
使用验证,而非事后截图。打开的下拉菜单、悬停状态或工具提示在截图运行时会消失;录制的帧才是真实状态。
check-beat.sh<select>Shadow DOM, tokens, and React fields
Shadow DOM、令牌和React字段
- Shadow DOM is invisible to selectors but not to the screen. A Modeler component panel put its
/
channel_idfields in a shadow root;textreturned 3 for the whole page while two more were plainly visible.document.querySelectorAll('input,textarea')needs only screen coordinates (from a screenshot), so click and type at raw coordinates, then verify by reading the saved config afterwards.xdotool - Native dropdowns DO record. They are invisible to the DOM, not to the screen -
<select>captures the open dropdown and every option fine. Drive them by type-ahead (click the select,x11grabthe option's visible label, press Return, which firestype64sochangeruns), and open the dropdown on camera when the options themselves are the point of the beat.#ajax - Typing opens a token browser that eats the rest of the line:
[leavesNode [node:nid]...in the field and the remainder in an "INSERT A TOKEN" popup. Insert via the clipboard, which fires no per-keystroke handlers:Node [agent-browser --cdp $CDP clipboard write "Node [node:nid] with [node:title] got updated." ./ui.sh key ctrl+v - React controlled inputs ignore . The
.value =trick (ui.sh paste+.valueevent) works for Drupal core forms but not React; for a React field set through the native setter and dispatch, or the component state never updates.input
- Shadow DOM对选择器不可见,但对屏幕可见。Modeler组件面板将其/
channel_id字段放在Shadow根中;text返回整个页面只有3个输入框,但实际上还有两个可见的输入框。document.querySelectorAll('input,textarea')只需要屏幕坐标(来自截图),因此可以直接点击和输入原始坐标,然后通过读取保存的配置进行验证。xdotool - 原生下拉菜单可以被录制。它们对DOM不可见,但对屏幕可见——
<select>可以捕获打开的下拉菜单和所有选项。通过预输入(点击选择框,x11grab输入选项的可见标签,按回车键,这会触发type64事件使change运行)来驱动它们;当选项本身是节拍的重点时,在屏幕上打开下拉菜单。见「Shadow DOM、令牌和React字段」。#ajax - 输入会打开令牌浏览器并吃掉剩余文本:输入
[会导致Node [node:nid]...留在字段中,剩余内容进入「INSERT A TOKEN」弹窗。通过剪贴板插入,这不会触发按键事件:Node [agent-browser --cdp $CDP clipboard write "Node [node:nid] with [node:title] got updated." ./ui.sh key ctrl+v - React受控输入忽略赋值。
.value =技巧(ui.sh paste+.value事件)对Drupal核心表单有效,但对React无效;对于React字段,需通过原生设置器赋值并触发事件,否则组件状态不会更新。input
Post-production: dead air and audio
后期处理:静音片段和音频
Run these once after all beats are recorded and narrated, before . Both fixed
user-visible defects on the first cut.
finish-all.shAudit the whole set first. compares video/speech/final duration
across every beat in one pass (a single run surfaced 27 pacing problems). It flags (too
little breath after narration) and (video running well past speech).
ddev exec bash $CDIR/audit.shAUDIO-TIGHTDEAD-AIRFade every narration tail. ElevenLabs gives no trailing decay - the last 150ms of every file
sits at -16..-29 dB, audibly clipped against the padded silence.
fades the last 120ms and appends real silence, always deriving from an untouched so a re-run
cannot double-fade. Do this before muxes the audio. Verify with
over the final 150ms: it should read about -91 dB.
ddev exec bash $CDIR/fade-audio.sh.origfinish-beat.shvolumedetectTrim trailing dead air, then verify the trim. Beats routinely ran 5-19s past the last on-screen
change because the capture slept waiting for a page. cuts the
frozen tail. Two tunings were hard-won:
ddev exec bash $CDIR/deadair.sh apply- , not the
freezedetect=n=-75dB:d=0.7default: at-58dBa checkbox tick counts as "frozen" and the trim silently cuts the click, ending the beat in the pre-click state. (A-58dBapproach was also tried and reported no changes at all - do not use it.)select='gt(scene,...)' - After each cut it compares the trimmed clip's last frame against the original's (, revert if < ~38 dB). A beat ending in the wrong state is worse than a slow beat.
psnr
Blind spot: a blinking text cursor in a focused input never registers as frozen, so those beats
need a manual head-trim and a human look at the end frame. Static slides shorter than ~6.5s are left
alone ( freeze-pads them back anyway).
finish-beat.shHead vs tail when trimming manually (): keep the tail when the payoff is the result
(install confirmation, saved message, JSON response); keep the head when the action is the content
(ticking boxes, typing, opening a picker). keeps so any cut can be redone.
trim.shtrim.shNN.orig.mp4在所有节拍录制和旁白生成完成后,运行前,执行以下步骤。这两个步骤都修复了第一版视频中用户可见的缺陷。
finish-all.sh先批量检查所有节拍。运行,一次性比较所有节拍的视频/旁白/最终时长(单次运行发现了27个节奏问题)。它会标记(旁白后留白过短)和(视频远长于旁白)的节拍。
ddev exec bash $CDIR/audit.shAUDIO-TIGHTDEAD-AIR为每个旁白结尾添加淡入淡出。ElevenLabs生成的旁白没有结尾衰减——每个文件的最后150ms处于-16..-29 dB,与添加的静音片段拼接时会出现明显的剪辑痕迹。运行为最后120ms添加淡入淡出效果,并补充真实静音片段,始终从未修改的文件生成,避免重复淡入淡出。请在混流音频之前执行此操作。使用验证最后150ms的音量:应约为-91 dB。
ddev exec bash $CDIR/fade-audio.sh.origfinish-beat.shvolumedetect裁剪末尾静音片段,然后验证裁剪结果。节拍通常会在最后一次屏幕变化后继续录制5-19秒,因为捕获脚本会等待页面加载。运行裁剪静止的结尾画面。以下两个参数是经过多次尝试确定的:
ddev exec bash $CDIR/deadair.sh apply- ,不要使用默认的
freezedetect=n=-75dB:d=0.7:设置为-58dB时,复选框的勾选会被判定为「静止」,裁剪会静默地剪掉点击动作,导致节拍以点击前的状态结束。(也曾尝试-58dB方法,但未报告任何变化——不要使用该方法。)select='gt(scene,...)' - 每次裁剪后,将裁剪后的片段最后一帧与原始片段的最后一帧进行比较(使用,如果值<约38 dB则恢复)。节拍以错误状态结束比节奏慢更糟糕。
psnr
盲区:聚焦输入框中的闪烁文本光标永远不会被判定为静止,因此这些节拍需要手动裁剪开头,并人工检查结尾帧。短于约6.5秒的静态幻灯片无需处理(会自动冻结画面补充时长)。
finish-beat.sh手动裁剪时保留开头还是结尾:当重点是结果(安装确认、保存成功消息、JSON响应)时,保留结尾;当重点是操作(勾选复选框、输入文本、打开选择器)时,保留开头。会保留,因此任何裁剪操作都可以重新执行。
trim.shNN.orig.mp4Recording gotchas
录制注意事项
General lessons for recording a Drupal admin UI in a headless browser:
- Clear text inputs before typing. GET filter and search fields keep their value across
reloads, so typing again appends ("Powered byPowered by") and the filter breaks. Clear
first with then type, or use
hands.sh key ctrl+a.agent-browser fill - Pre-seed AJAX-dependent forms. Forms that rebuild dependent fields via Drupal AJAX (a
provider select that repopulates a model select, etc.) are unreliable to drive live. Set
the value first with so the form loads already settled, then only demonstrate the final selection on camera.
drush config:set - Avoid batch operations on camera. Actions that trigger a batch (some imports, adding a
language with interface translation) rely on a meta-refresh that stalls in the headless
browser. Disable or pre-run the batch with before recording so the page redirects instantly.
drush - Target fields by name, not id. Drupal rebuilds regenerate element ids, so a selector grabbed before the rebuild goes stale. Use
#ajax.getElementsByName('...')[0] - Drive native by type-ahead (click,
<select>the visible label, Return). The dropdown is invisible to the DOM so clicking options directly fails, but it does record on screen - open it on camera when the options are the point. See "Shadow DOM, tokens, and React fields".type64 - Paste long text, don't type it. A ~1800-char field typed key-by-key is a >2-minute beat.
Set via
.value(base64 in,evalin the page), dispatchatob+input, and narrate it as "paste in...".change - Never put control flow or in a container command. See "Getting commands past
$(...)": keep eachddev execto one simple statement, and type throughcexec.type64
在无头浏览器中录制Drupal管理UI的通用经验:
- 输入前清空文本框。GET过滤器和搜索字段会在重载后保留值,因此再次输入会追加内容(例如"Powered byPowered by"),导致过滤器失效。先使用清空,然后输入,或使用
hands.sh key ctrl+a。agent-browser fill - 预填充依赖AJAX的表单。通过Drupal AJAX重建依赖字段的表单(例如选择提供商后重新填充模型选择框等),实时驱动不可靠。先使用设置值,使表单加载时已处于稳定状态,然后仅在屏幕上演示最终选择。
drush config:set - 避免在屏幕上录制批处理操作。触发批处理的操作(某些导入、添加带界面翻译的语言)依赖元刷新,这在无头浏览器中会停滞。在录制前禁用或使用预运行批处理,使页面立即重定向。
drush - 按名称而非ID定位字段。Drupal 重建会重新生成元素ID,因此重建前获取的选择器会失效。使用
#ajax。getElementsByName('...')[0] - 通过预输入驱动原生(点击,
<select>输入可见标签,按回车键)。下拉菜单对DOM不可见,因此直接点击选项会失败,但它可以被屏幕录制——当选项本身是节拍的重点时,在屏幕上打开下拉菜单。见「Shadow DOM、令牌和React字段」。type64 - 粘贴长文本,不要逐字输入。输入约1800字符的字段会生成超过2分钟的节拍。通过设置
eval(主机上base64编码,页面内.value解码),触发atob+input事件,并旁白为「粘贴内容...」。change - 不要在容器命令中包含控制流或。见「让命令通过
$(...)正常执行」:每个ddev exec保持为单个简单语句,通过cexec输入文本。type64
Known tuning points (verify on the first live run)
已知调试点(首次运行时验证)
- CDP: use the container. Host CDP does not work: ddev maps the exposed port to a dynamic
host port, and Chromium's DevTools rejects the forwarded connection because the Host-header
port no longer matches its listening port (DNS-rebinding protection; only covers Origin, not Host). Always drive agent-browser inside the container against localhost:
--remote-allow-origins=*.ddev exec agent-browser --cdp http://127.0.0.1:9222 ... - Cursor alignment. returns viewport coordinates. Kiosk Chromium at 0,0 with
get boxmakes viewport pixels equal screen pixels, but a small fixed offset may be needed. Take a screenshot mid-beat and adjust if the click misses.--force-device-scale-factor=1 - Window focus for typing. goes to the focused window. session.sh activates the Chromium window; if typing lands nowhere, re-activate it before typing.
xdotool type - Non-Latin languages need CJK fonts. Preflight installs so Japanese, Chinese, and Korean render instead of tofu boxes. Chromium caches fonts at startup, so if you install fonts after a session is running, restart it (
fonts-noto-cjk); a page reload is not enough.session.sh stop && start - drupal.org serves the kiosk browser HTTP 406. Looks like TLS or client-hint fingerprinting:
from the same container with Chrome-like headers gets 200, the browser (a normal
curlUA) does not, and UA /Chrome/*/Accept*flags do not fix it. Treat--langandintrobeats that openmodule-pageas possibly unrecordable, and have a local-slide fallback ready (see the intro caveat under Beat taxonomy).drupal.org
- CDP:使用容器内的服务。主机CDP无法工作:ddev将暴露的端口映射到动态主机端口,Chromium的DevTools会拒绝转发连接,因为Host头端口不再匹配其监听端口(DNS重绑定保护;仅覆盖Origin,不覆盖Host)。始终在容器内运行agent-browser并连接到本地服务:
--remote-allow-origins=*。ddev exec agent-browser --cdp http://127.0.0.1:9222 ... - 光标对齐。返回视口坐标。设置
get box的kiosk模式Chromium位于0,0,使视口像素与屏幕像素相等,但可能需要微调固定偏移。在节拍录制过程中截取屏幕截图,若点击位置不准确则调整偏移。--force-device-scale-factor=1 - 打字时的窗口焦点。会将输入发送到聚焦的窗口。session.sh会激活Chromium窗口;如果输入无响应,在输入前重新激活窗口。
xdotool type - 非拉丁语系语言需要CJK字体。预检查脚本会安装,使日语、中文和韩语正常显示而非方块字符。Chromium在启动时缓存字体,因此如果在会话运行后安装字体,需重启会话(
fonts-noto-cjk);仅重载页面无效。session.sh stop && start - drupal.org向kiosk浏览器返回HTTP 406。看起来是TLS或客户端提示指纹识别问题:从同一容器使用类Chrome头信息执行可得到200响应,但使用正常
curlUA的浏览器无法访问,且修改UA、Chrome/*、Accept*参数均无法解决。将打开--lang的drupal.org和intro类型节拍视为可能无法录制,并准备好本地幻灯片作为备用方案(见「节拍分类」中的介绍类节拍注意事项)。module-page
Common mistakes
常见错误
| Mistake | Fix |
|---|---|
Using | It has no TTS. Use |
| agent-browser doing the click | CDP clicks are invisible in the recording. Click with |
| Recording a terminal | Terminal commands are command cards, not screen recordings. |
| Intro on the generic drupal.org site | The intro shows the module's own project page, |
| Ephemeral container packages | Installs are lost on |
| Cleaning up before approval | Leave the build dir intact until the user approves. |
| Narration and video out of sync | Author one sentence + one action per beat; |
| Driving agent-browser from the host | Host CDP is blocked; run it in the container: |
| Typing into a field that still holds text | Clear it first ( |
| Recording an AJAX select or batch page live | Pre-seed with |
| Caption omits where the step happens | For any navigation or route action, show the admin path or link in the caption (e.g. |
Wrong | Use |
Loops or | The container shell mangles them before running. Keep container commands to one simple statement; loop on the host. |
| Braces, backslashes, and quotes get mangled by |
| Selecting by element id after an AJAX rebuild | Ids regenerate. Target by name ( |
| Typing a long prompt key-by-key | Minutes-long beat. Paste via |
| Numbers with comma decimals reaching ffmpeg | A comma-decimal locale breaks the filtergraph. Scripts export |
Re-running | The old container ffmpeg keeps writing; a second racing encode corrupts |
| Trusting "file exists" as done | Verify with |
| Off-viewport click that silently no-ops | Below y≈1000 / above y≈80 misses the 1080 viewport and never submits. |
| Every Drupal submit is |
| Substring text match on an admin page | "Lock" matches "Blocks". Use |
Believing a | It records fine; it is invisible to the DOM, not the screen. Drive by type-ahead. |
| Installing a module set with no admin UI | List the routes later scenes need and check each resolves; the UI may be a separate submodule (e.g. |
| The package has unpushed local commits. Bundle it, check out the locked ref, then require. |
| Narration tail sounds clipped | ElevenLabs has no decay; run |
| Trimming dead air without verifying | |
| Numbering beats 1,2,3 with no gaps | Reorders arrive after recording and there is no room to insert. Number in 10s, or swap adjacent beats. |
| Hardcoding the scene-final beat list | Reordering leaves the pause on the wrong beat. Derive it ( |
| 错误 | 修复方法 |
|---|---|
使用 | 该工具无TTS功能。使用 |
| 用agent-browser执行点击 | CDP点击在录制中不可见。使用 |
| 录制终端画面 | 终端命令应生成为命令卡片,而非屏幕录制。 |
| 介绍类内容使用通用drupal.org站点 | 介绍类内容应展示模块自身的项目页面: |
| 容器包临时安装 | 除非安装到 |
| 用户确认前清理文件 | 在用户确认前保留构建目录。 |
| 旁白与画面不同步 | 每个节拍对应一句旁白+一个操作; |
| 从主机驱动agent-browser | 主机CDP被阻止;在容器内运行: |
| 向已有文本的字段输入内容 | 先清空字段( |
| 实时录制AJAX选择框或批处理页面 | 使用 |
| 字幕未说明步骤所在位置 | 对于任何导航或路由操作,在字幕中显示管理路径或链接(例如 |
| 使用 |
在 | 容器shell会在执行前破坏这些内容。每个容器命令保持为单个简单语句;在主机上执行循环。 |
使用 | 大括号、反斜杠和引号会被 |
| AJAX重建后按元素ID选择 | ID会重新生成。按名称定位( |
| 逐字输入长提示文本 | 生成的节拍时长过长。通过 |
| 向ffmpeg传入带逗号的小数 | 逗号小数的区域设置会破坏滤镜图。脚本会导出 |
终止 | 旧的容器ffmpeg进程会继续写入;并行的第二次编码会损坏 |
| 以「文件存在」作为完成标志 | 使用 |
| 视口外点击导致静默失败 | 低于y≈1000 / 高于y≈80的点击会错过1080视口,永远不会提交。 |
使用 | 每个Drupal提交按钮的name都是 |
| 在管理页面上使用子字符串文本匹配 | "Lock"会匹配到"Blocks"。对于短单词,优先使用 |
认为 | 它可以被正常录制;它对DOM不可见,但对屏幕可见。通过预输入驱动。 |
| 安装无管理UI的模块集 | 列出后续场景依赖的路由并检查每个路由是否可访问;管理UI可能存在于单独的子模块中(例如 |
基于Git检出的贡献模块执行 | 该包存在未推送的本地提交。先打包备份,切换到锁定的引用,然后执行require。 |
| 旁白结尾有剪辑痕迹 | ElevenLabs生成的旁白没有衰减;在完成节拍处理前运行 |
| 未验证就裁剪静音片段 | |
| 节拍编号为1,2,3且无间隙 | 修改顺序的需求在录制完成后提出,无法插入新节拍。以10为步长编号,或交换相邻节拍。 |
| 硬编码场景结尾节拍列表 | 修改顺序后,留白会添加到错误的节拍上。自动推导( |