storytellurx/scripts/product-demo/capture.ts

125 lines
4.7 KiB
TypeScript

import { demoScenes } from "./scenes.ts";
import { z } from "zod";
import { chromium } from "@playwright/test";
import { mkdir, writeFile, access } from "node:fs/promises";
const browser = await chromium.launch();
const people = demoScenes.map((scene) => scene.id);
const scenes = demoScenes.map((scene) => scene.prompt);
for (const [index, id] of people.entries()) {
if (process.env["ONLY"] && id !== process.env["ONLY"]) continue;
const dir = `reports/image-reveal-demo/${id}`;
if (
!process.env["ONLY"] &&
(await access(`${dir}/generated.webp`).then(
() => true,
() => false,
))
)
continue;
await mkdir(dir, { recursive: true });
const context = await browser.newContext({
viewport: { width: 540, height: 900 },
reducedMotion: "no-preference",
recordVideo: { dir, size: { width: 540, height: 900 } },
});
await context.addInitScript(
({ id }) => {
sessionStorage.setItem(
`stx:onboarding:v2:demo-${id}`,
JSON.stringify({
version: 2,
locale: "fr",
intensity: "beginner",
adultConfirmed: true,
warningsAccepted: true,
participationConfirmed: true,
}),
);
localStorage.setItem("stx:metrics-tour:completed:v2", "true");
},
{ id },
);
const recordingStarted = Date.now();
const page = await context.newPage();
const errors: string[] = [];
page.on("pageerror", (e) => errors.push(e.message));
// The demo uses explicit, nonfigurative art direction. The real image endpoint and Rev Animated run unchanged.
await page.route("**/api/ai/image", async (route) => {
const data = z
.object({
forwardedProps: z.record(z.string(), z.unknown()),
data: z.record(z.string(), z.unknown()),
})
.loose()
.parse(route.request().postDataJSON());
const prompt = `${scenes[index]}, atmospheric concept art, vaporous and dreamy, painterly illustration, soft bloom, no people, no bodies, no text`;
data.forwardedProps["prompt"] = prompt;
data.data["prompt"] = prompt;
data.forwardedProps["negativePrompt"] =
"people, bodies, nudity, sexual content, text, watermark, sharp noise";
data.data["negativePrompt"] = data.forwardedProps["negativePrompt"];
await writeFile(
`${dir}/image-request.json`,
JSON.stringify(
{
model: "rev-animated",
steps: 3,
cfgScale: 1,
prompt,
negativePrompt: data.data["negativePrompt"],
},
null,
2,
),
);
const response = await route.fetch({ postData: JSON.stringify(data), timeout: 180000 });
const body = await response.text();
await writeFile(`${dir}/image.sse`, body);
await route.fulfill({ response, body });
});
page.on("response", async (response) => {
if (response.url().endsWith("/api/ai/chat")) {
try {
await writeFile(`${dir}/chat.sse`, await response.text());
} catch {}
}
});
await page.goto(
`http://127.0.0.1:3000/chat?persona-id=${id}&model-id=gemini&gateway-id=nanogpt&session-id=demo-${id}`,
);
const input = page.getByRole("textbox");
await input.waitFor();
await input.fill(
"Je choisis le mode texte et image. Ouvre un paysage de rêve, dans la brume. Deux phrases sur les étoiles, sans personnages ni contenu sexuel, et une illustration.",
);
await input.press("Enter");
try {
await page.locator('[data-image-reveal="summon"]').waitFor({ timeout: 180000 });
await writeFile(
`${dir}/timing.json`,
JSON.stringify({ summon: Date.now() - recordingStarted }),
);
await page.waitForTimeout(550);
await page.screenshot({ path: `${dir}/summon.png` });
await page.locator('[data-image-reveal="reveal"]').waitFor();
await page.waitForTimeout(1700);
await page.screenshot({ path: `${dir}/reveal.png` });
await page.locator("[data-image-reveal]").waitFor({ state: "detached", timeout: 15000 });
await page.waitForTimeout(1500);
await page.screenshot({ path: `${dir}/dialogue.png` });
const image = page.locator("[data-image-slot] img").last();
const src = await image.getAttribute("src");
if (src?.startsWith("data:"))
await writeFile(`${dir}/generated.webp`, Buffer.from(src.split(",")[1]!, "base64"));
console.log(`${id}: live image and screenshots captured`);
} catch (error) {
console.log(`${id}: ${error instanceof Error ? error.message : String(error)}`);
await page.screenshot({ path: `${dir}/failure.png` });
}
await writeFile(`${dir}/page.txt`, await page.locator("body").innerText());
await writeFile(`${dir}/errors.json`, JSON.stringify(errors));
const video = page.video();
await context.close();
await video?.saveAs(`${dir}/session.webm`);
}
await browser.close();