storytellurx/scripts/product-demo/assemble.ts

148 lines
6.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { demoScenes, demoDesign } from "./scenes.ts";
import { z } from "zod";
import { chromium } from "@playwright/test";
import { readFile, writeFile, mkdir } from "node:fs/promises";
import { spawnSync } from "node:child_process";
import path from "node:path";
const root = path.resolve("reports/image-reveal-demo");
const scenes = await Promise.all(
demoScenes.map(
async (scene) =>
[
scene.id,
scene.label,
demoDesign(scene.id).title[0],
z
.object({ ink: z.string() })
.parse(JSON.parse(await readFile(`${root}/${scene.id}/product-timing.json`, "utf8"))).ink,
scene.description,
] as const,
),
);
const browser = await chromium.launch();
const page = await browser.newPage({
viewport: { width: 1440, height: 900 },
deviceScaleFactor: 1,
});
const font = (await readFile("public/assets/fonts/Arizona/ABCArizonaPlusVariable.woff2")).toString(
"base64",
);
const mono = (await readFile("public/assets/fonts/IBMPlexMono/IBMPlexMono-Regular.woff2")).toString(
"base64",
);
const css = `@font-face{font-family:Arizona;src:url(data:font/woff2;base64,${font})}@font-face{font-family:Mono;src:url(data:font/woff2;base64,${mono})}*{box-sizing:border-box}body{margin:0;width:1440px;height:900px;background:#121019;color:#f8f1e8;font-family:Arizona;overflow:hidden}main{position:absolute;inset:0;padding:70px 80px;background:radial-gradient(ellipse at 80% 42%,color-mix(in srgb,var(--ink) 14%,transparent),transparent 58%)}header{font:14px Mono;letter-spacing:.1em;display:flex;justify-content:space-between;color:#bbb3c6}.eyebrow{font:13px Mono;letter-spacing:.18em;color:var(--ink)}h1{font-size:85px;line-height:.98;font-weight:380;margin:28px 0;max-width:760px;letter-spacing:-3px}h2{font-size:50px;font-weight:400;margin:20px 0}p{font-size:25px;line-height:1.4;color:#bdb6c7;max-width:660px}footer{position:absolute;bottom:64px;left:80px;right:80px;font:11px Mono;color:#aaa2b7;display:flex;justify-content:space-between}hr{border:0;border-top:1px solid #ffffff24;margin:28px 0;width:620px}.emblem{width:100px;height:100px;margin-bottom:30px;filter:drop-shadow(0 0 25px var(--ink))}.badge{font:12px Mono;border:1px solid #ffffff35;border-radius:30px;padding:12px 18px;display:inline-block;color:var(--ink)}.steps{display:flex;gap:22px;font:12px Mono;color:#c2bacc;margin-top:40px}.steps b{color:var(--ink)}.dots{display:flex;gap:8px;margin-top:35px}.dots i{width:34px;height:3px;background:#ffffff26}.dots i.active{background:var(--ink)}`;
await mkdir(`${root}/film`, { recursive: true });
async function background(name: string, content: string, ink = "#d5adff") {
await page.setContent(
`<style>${css}</style><main style="--ink:${ink}"><header><span>STORYTELLURX / 2.0</span><span>HUIT MONDES · HUIT VISIONS</span></header>${content}<footer><span>REV ANIMATED · IMAGES GÉNÉRÉES</span><span>DÉMONSTRATION SCÉNARISÉE · CAPTURE DE L’APPLICATION</span></footer></main>`,
);
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: `${root}/film/${name}.png` });
}
function ffmpeg(args: string[]) {
const run = spawnSync("ffmpeg", ["-hide_banner", "-loglevel", "error", "-y", ...args], {
stdio: "inherit",
});
if (run.status !== 0) throw new Error("ffmpeg failed");
}
await background(
"intro",
'<div style="margin-top:135px"><div class="eyebrow">DES RÉCITS QUI PRENNENT FORME</div><h1>Et soudain,<br>une vision.</h1><p>Choisissez un monde. Entrez dans le récit.<br>Découvrez l’image comme un objet précieux.</p><div class="steps"><b>01</b> CHOISIR <b>02</b> EXPLORER <b>03</b> DÉCOUVRIR</div></div>',
);
ffmpeg([
"-loop",
"1",
"-i",
`${root}/film/intro.png`,
"-t",
"4",
"-vf",
"fade=t=in:d=0.4,fade=t=out:st=3.6:d=0.4",
"-r",
"30",
"-c:v",
"libx264",
"-pix_fmt",
"yuv420p",
`${root}/film/00-intro.mp4`,
]);
const files = [`${root}/film/00-intro.mp4`];
for (const [index, [id, label, title, ink, description]] of scenes.entries()) {
const timing = z
.object({ summon: z.number(), video: z.string() })
.parse(JSON.parse(await readFile(`${root}/${id}/product-timing.json`, "utf8")));
await background(
id,
`<div style="margin-top:105px"><div class="eyebrow">MONDE ${String(index + 1).padStart(2, "0")} / ${label}</div><h1 style="font-size:70px;max-width:710px">${title}</h1><p>${description}</p><hr><span class="badge">UNE IMAGE COMME UNE DÉCOUVERTE</span><div class="steps">APPARITION <b>→</b> RÉVÉLATION <b>→</b> RETOUR AU RÉCIT</div><div class="dots">${scenes.map((_, i) => `<i class="${i === index ? "active" : ""}"></i>`).join("")}</div></div>`,
ink,
);
const video = path.isAbsolute(timing.video) ? timing.video : path.resolve(timing.video);
const out = `${root}/film/${String(index + 1).padStart(2, "0")}-${id}.mp4`;
ffmpeg([
"-loop",
"1",
"-i",
`${root}/film/${id}.png`,
"-ss",
String(Math.max(0, timing.summon / 1000 - 1.1)),
"-i",
video,
"-filter_complex",
"[1:v]scale=432:720,setsar=1[app];[0:v][app]overlay=910:90:shortest=1,fade=t=in:d=0.3,fade=t=out:st=9.2:d=0.4[v]",
"-map",
"[v]",
"-t",
"9.6",
"-r",
"30",
"-an",
"-c:v",
"libx264",
"-crf",
"19",
"-pix_fmt",
"yuv420p",
out,
]);
files.push(out);
console.log(`${id}: film segment assembled`);
}
await background(
"outro",
'<div style="margin-top:155px"><div class="eyebrow">STORYTELLURX</div><h1>Votre prochain<br>monde vous attend.</h1><p>Huit personas. Huit matières.<br>Un même rituel de découverte.</p><div class="badge" style="margin-top:25px">DU CHOIX DU MONDE AU RÉCIT, DIRECTEMENT.</div></div>',
);
ffmpeg([
"-loop",
"1",
"-i",
`${root}/film/outro.png`,
"-t",
"4",
"-vf",
"fade=t=in:d=0.4,fade=t=out:st=3.6:d=0.4",
"-r",
"30",
"-c:v",
"libx264",
"-pix_fmt",
"yuv420p",
`${root}/film/09-outro.mp4`,
]);
files.push(`${root}/film/09-outro.mp4`);
await writeFile(`${root}/film/concat.txt`, files.map((file) => `file '${file}'`).join("\n"));
ffmpeg([
"-f",
"concat",
"-safe",
"0",
"-i",
`${root}/film/concat.txt`,
"-c",
"copy",
"-movflags",
"+faststart",
`${root}/storytellurx-product-demo.mp4`,
]);
await browser.close();
console.log("Product film complete");