mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
fix(ci): cut per-scene page loads out of ui-preview videos
This commit is contained in:
@@ -24,7 +24,9 @@ carries the `ui-preview` label), the workflow does this:
|
||||
- has it plan scenes: what to click, double-click, hover, type and capture;
|
||||
- walks the plan in Chromium with `cursor.js` drawing the pointer, its trail and each
|
||||
click. It always takes screenshots. When the plan interacts, it also records an mp4,
|
||||
never a GIF. A failed step is re-planned from the page as it is (up to three times),
|
||||
never a GIF — each scene on a page of its own, its load trimmed off and the takes
|
||||
joined, so scene changes read as cuts rather than the app reloading. A failed step
|
||||
is re-planned from the page as it is (up to three times),
|
||||
after an Escape clears anything the failure left open (a menu, a dialog); what still
|
||||
fails is listed honestly in the PR block, and shots taken after a failure in their
|
||||
scene are marked ⚠️ both there and on the Pages player.
|
||||
|
||||
@@ -147,7 +147,7 @@ video,img{display:block;width:100%;border-radius:10px;border:1px solid var(--lin
|
||||
a{color:inherit}</style>
|
||||
<main><h1>#${pr} 界面预览</h1><p class="m">commit ${short(sha)} · <a href="https://github.com/${repo}/pull/${pr}">回到 PR</a>${m.summary ? " · " + esc(m.summary) : ""}</p>
|
||||
${warned ? `<p class="warn">⚠️ 这次录制有 ${m.errors?.length || 0} 步没能按计划执行${shots.some((x) => x.warn) ? ";带 ⚠️ 的截图拍摄于步骤失败之后,画面未必是说明文字所描述的状态" : ""}。</p>` : ""}
|
||||
${m.video ? `<figure><video src="${m.video}" controls autoplay muted playsinline poster="${m.poster || ""}"></video><figcaption>红线是鼠标走过的轨迹,红色圆环是一次点击;底部文字是这一步在做什么。空格暂停,← → 逐段查看。</figcaption></figure>` : ""}
|
||||
${m.video ? `<figure><video src="${m.video}" controls autoplay muted playsinline poster="${m.poster || ""}"></video><figcaption>红线是鼠标走过的轨迹,红色圆环是一次点击;底部文字是这一步在做什么。多个场景各自从应用初始状态开始。空格暂停,← → 逐段查看。</figcaption></figure>` : ""}
|
||||
${shots.map((x) => `<figure${x.warn ? ' class="shaky"' : ""}><img src="${esc(x.file)}" alt="${esc(x.caption)}"><figcaption>${x.warn ? "⚠️ " : ""}${esc(x.scene)} · ${esc(x.caption)}</figcaption></figure>`).join("\n")}
|
||||
</main></html>`;
|
||||
}
|
||||
|
||||
@@ -582,19 +582,33 @@ async function main() {
|
||||
if (plan.scenes.length) {
|
||||
const rec = await browser.newContext({ ...ctxOpts, recordVideo: { dir: path.join(OUT, "raw"), size: VIEW } });
|
||||
await rec.addInitScript({ path: path.join(here, "cursor.js") });
|
||||
const page = await rec.newPage();
|
||||
const began = Date.now();
|
||||
let lead = 0, repairs = 0, interactive = false;
|
||||
let repairs = 0, interactive = false;
|
||||
// one take per scene: every scene starts from the app as it opens, and
|
||||
// its video keeps only what happens once the app has settled — the
|
||||
// load never reaches the final cut, so a scene change reads as a cut,
|
||||
// not as the app reloading out of nowhere
|
||||
const takes = [];
|
||||
for (const s of plan.scenes.slice(0, 5)) {
|
||||
const scene = { title: s.title || "", shots: [], derailed: false };
|
||||
manifest.scenes.push(scene);
|
||||
const page = await rec.newPage();
|
||||
const began = Date.now();
|
||||
let ready = 0;
|
||||
try {
|
||||
await caption(page, scene.title);
|
||||
await page.goto(BASE);
|
||||
await settle(page);
|
||||
if (!lead) lead = Math.max(0, (Date.now() - began) / 1000 - 0.4);
|
||||
await page.evaluate(([x, y]) => window.__uiPreviewAt?.(x, y), [mouse.x, mouse.y]);
|
||||
} catch (e) { manifest.errors.push(`${scene.title}: open: ${e.message}`); if (manifest.leak) break; continue; }
|
||||
ready = Date.now();
|
||||
} catch (e) {
|
||||
manifest.errors.push(`${scene.title}: open: ${e.message}`);
|
||||
await page.close().catch(() => {});
|
||||
if (manifest.leak) break;
|
||||
continue;
|
||||
}
|
||||
// the scene's title readable on the settled page for a beat, before
|
||||
// the first step's caption replaces it
|
||||
await caption(page, scene.title);
|
||||
await sleep(1100);
|
||||
let steps = (s.steps || []).slice(0, 25);
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
const step = steps[i];
|
||||
@@ -632,17 +646,23 @@ async function main() {
|
||||
} catch (e2) { manifest.errors.push(`repair: ${e2.message}`); }
|
||||
}
|
||||
}
|
||||
if (manifest.leak) break;
|
||||
if (manifest.leak) { await page.close().catch(() => {}); break; }
|
||||
await caption(page, "");
|
||||
await sleep(600);
|
||||
takes.push({ video: await page.video().path(), from: Math.max(0, (ready - began) / 1000 - 0.2) });
|
||||
await page.close();
|
||||
}
|
||||
const video = await page.video().path();
|
||||
await rec.close();
|
||||
manifest.interactive = interactive;
|
||||
// a video when there's something to watch: an interaction
|
||||
if (!manifest.leak && interactive) {
|
||||
execFileSync("ffmpeg", ["-y", "-loglevel", "error", "-ss", lead.toFixed(2), "-i", video, "-c:v", "libx264", "-preset", "medium", "-crf", "20",
|
||||
"-pix_fmt", "yuv420p", "-movflags", "+faststart", "-r", "30", path.join(OUT, "preview.mp4")]);
|
||||
if (!manifest.leak && interactive && takes.length) {
|
||||
// the takes joined, each already trimmed of its load
|
||||
const args = ["-y", "-loglevel", "error"];
|
||||
const pins = takes.map((t, i) => (args.push("-ss", t.from.toFixed(2), "-i", t.video), `[${i}:v]`));
|
||||
args.push("-filter_complex", `${pins.join("")}concat=n=${takes.length}:v=1:a=0[v]`, "-map", "[v]",
|
||||
"-c:v", "libx264", "-preset", "medium", "-crf", "20", "-pix_fmt", "yuv420p", "-movflags", "+faststart", "-r", "30",
|
||||
path.join(OUT, "preview.mp4"));
|
||||
execFileSync("ffmpeg", args);
|
||||
manifest.video = "preview.mp4";
|
||||
manifest.poster = await poster(browser);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user