-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Expand file tree
/
Copy pathpreview-gifs.js
More file actions
108 lines (94 loc) · 3.39 KB
/
preview-gifs.js
File metadata and controls
108 lines (94 loc) · 3.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
#!/usr/bin/env node
/**
* Extract a preview frame from each demo GIF for quick visual inspection.
* Saves individual PNG frames to demo-previews/ directory.
*
* Requires: ffmpeg, gifsicle (for frame delay info)
*
* Usage:
* node preview-gifs.js # default: 3s before end
* node preview-gifs.js --before 5 # 5s before end
*/
const { execSync } = require('child_process');
const { readdirSync, existsSync, mkdirSync, rmSync } = require('fs');
const { join, basename } = require('path');
const rootDir = join(__dirname, '..', '..');
const previewDir = join(rootDir, 'demo-previews');
// Parse CLI args
const args = process.argv.slice(2);
let beforeSeconds = 3;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--before' && args[i + 1]) {
beforeSeconds = parseFloat(args[++i]);
}
}
// Find all demo GIFs
function findGifs() {
const gifs = [];
for (const entry of readdirSync(rootDir)) {
if (!/^\d{2}-/.test(entry)) continue;
const imagesDir = join(rootDir, entry, 'images');
if (!existsSync(imagesDir)) continue;
for (const file of readdirSync(imagesDir)) {
if (file.endsWith('-demo.gif')) {
gifs.push({ path: join(imagesDir, file), chapter: entry });
}
}
}
return gifs.sort((a, b) => a.path.localeCompare(b.path));
}
// Get frame delays from a GIF
function getFrameDelays(gifPath) {
const output = execSync(`gifsicle --info "${gifPath}"`, { encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 });
const delays = [];
const delayRegex = /delay (\d+(?:\.\d+)?)s/g;
let match;
while ((match = delayRegex.exec(output)) !== null) {
delays.push(parseFloat(match[1]));
}
return delays;
}
// Find frame index at N seconds before the end
function frameAtSecondsBeforeEnd(delays, seconds) {
const totalTime = delays.reduce((a, b) => a + b, 0);
const targetTime = totalTime - seconds;
if (targetTime <= 0) return 0;
let cumulative = 0;
for (let i = 0; i < delays.length; i++) {
cumulative += delays[i];
if (cumulative >= targetTime) return i;
}
return delays.length - 1;
}
// Main
if (existsSync(previewDir)) rmSync(previewDir, { recursive: true });
mkdirSync(previewDir, { recursive: true });
const gifs = findGifs();
if (gifs.length === 0) {
console.log('No demo GIFs found');
process.exit(0);
}
console.log(`\nExtracting frames (${beforeSeconds}s before end) from ${gifs.length} GIFs...\n`);
let count = 0;
for (const { path: gif, chapter } of gifs) {
const name = basename(gif, '.gif');
const delays = getFrameDelays(gif);
const frameIndex = frameAtSecondsBeforeEnd(delays, beforeSeconds);
const prefix = chapter.replace(/^(\d+)-.+/, '$1');
const outName = `${prefix}-${name}.png`;
const outPath = join(previewDir, outName);
try {
execSync(
`ffmpeg -y -i "${gif}" -vf "select=eq(n\\,${frameIndex})" -vframes 1 -update 1 "${outPath}" 2>/dev/null`,
{ stdio: 'pipe' }
);
console.log(` ✓ ${outName} (frame #${frameIndex}/${delays.length})`);
count++;
} catch (e) {
console.log(` ✗ ${name}: extraction failed`);
}
}
console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
console.log(`✓ ${count} preview frames saved to demo-previews/`);
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
console.log(`\nOpen in Finder: open demo-previews/`);