Files
kandev/apps/web/e2e/scripts/convert-assets.ts
T

124 lines
3.8 KiB
TypeScript

#!/usr/bin/env tsx
/**
* Post-e2e script: converts frame sequences to GIFs using ffmpeg.
* Falls back gracefully if ffmpeg is not available.
*
* Usage: npx tsx e2e/scripts/convert-assets.ts
*/
import fs from "node:fs";
import path from "node:path";
import { execSync } from "node:child_process";
import type { AssetManifest } from "../helpers/pr-asset-capture";
const ASSETS_DIR = path.resolve(__dirname, "../../.pr-assets");
const FRAMES_DIR = path.join(ASSETS_DIR, ".frames");
const MANIFEST_PATH = path.join(ASSETS_DIR, "manifest.json");
const GIF_WIDTH = 800;
const GIF_FPS = 5;
function hasFfmpeg(): boolean {
try {
execSync("ffmpeg -version", { stdio: "ignore" });
return true;
} catch {
return false;
}
}
function convertFramesToGif(framesDir: string, outputPath: string): boolean {
try {
const cmd = [
"ffmpeg",
"-y",
`-framerate ${GIF_FPS}`,
`-i "${path.join(framesDir, "frame-%04d.png")}"`,
`-vf "fps=${GIF_FPS},scale=${GIF_WIDTH}:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse"`,
"-loop 0",
`"${outputPath}"`,
].join(" ");
execSync(cmd, { stdio: "ignore" });
return true;
} catch (err) {
console.error(`Failed to convert frames to GIF: ${outputPath}`, err);
return false;
}
}
function cleanupFrames(recordingName: string): void {
const framesPath = path.join(FRAMES_DIR, recordingName);
if (fs.existsSync(framesPath)) {
fs.rmSync(framesPath, { recursive: true });
}
}
function main(): void {
if (!fs.existsSync(MANIFEST_PATH)) {
console.log("No manifest.json found — nothing to convert.");
return;
}
const manifest: AssetManifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, "utf-8"));
const recordings = manifest.assets.filter((a) => a.type === "recording");
if (recordings.length === 0) {
console.log("No recordings to convert.");
return;
}
const ffmpegAvailable = hasFfmpeg();
if (!ffmpegAvailable) {
console.warn("ffmpeg not found — skipping GIF conversion. Install ffmpeg for GIF support.");
// Keep recordings in manifest but mark as unconverted
for (const rec of recordings) {
const framesDir = path.join(FRAMES_DIR, rec.name);
if (!fs.existsSync(framesDir)) continue;
// Fall back: use first frame as a static screenshot
const firstFrame = path.join(framesDir, "frame-0000.png");
if (fs.existsSync(firstFrame)) {
const fallbackName = rec.file.replace(".gif", ".png");
fs.copyFileSync(firstFrame, path.join(ASSETS_DIR, fallbackName));
rec.file = fallbackName;
rec.format = "png";
rec.type = "screenshot";
console.log(` Fallback: ${rec.name} → ${fallbackName} (first frame)`);
}
cleanupFrames(rec.name);
}
manifest.generated_at = new Date().toISOString();
fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
return;
}
let converted = 0;
for (const rec of recordings) {
const framesDir = path.join(FRAMES_DIR, rec.name);
if (!fs.existsSync(framesDir)) {
console.warn(` Frames directory not found for: ${rec.name}`);
continue;
}
const outputPath = path.join(ASSETS_DIR, rec.file);
console.log(` Converting: ${rec.name} → ${rec.file}`);
if (convertFramesToGif(framesDir, outputPath)) {
converted++;
cleanupFrames(rec.name);
}
}
// Clean up .frames dir if empty
if (fs.existsSync(FRAMES_DIR)) {
const remaining = fs.readdirSync(FRAMES_DIR);
if (remaining.length === 0) {
fs.rmSync(FRAMES_DIR, { recursive: true });
}
}
manifest.generated_at = new Date().toISOString();
fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
console.log(`Converted ${converted}/${recordings.length} recordings to GIF.`);
}
main();