124 lines
3.8 KiB
TypeScript
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();
|