fp/services/our/src/utils/storyboard2.ts
CJ_Clippy b7a64d1fd3
Some checks failed
ci / build (push) Failing after 1m22s
ci / Tests & Checks (push) Failing after 1s
progress
2025-08-10 18:17:27 -08:00

143 lines
4.4 KiB
TypeScript

// storyboard2.ts
// ported from https://github.com/Dibyakshu/go-video-storyboard-generator/blob/main/main.go
import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'fs';
import { join } from 'path';
import { getNanoSpawn } from './nanoSpawn';
// Entry point
(async function main() {
if (process.argv.length < 5) {
showUsage();
}
const fps = parseInt(process.argv[2], 10);
const inputFile = process.argv[3];
const outputDir = process.argv[4];
if (isNaN(fps) || fps <= 0) {
console.error('Error: Invalid FPS value. Please provide a positive integer.');
process.exit(1);
}
ensureDirectoryExists(outputDir);
const startTime = Date.now();
const thumbnailsDir = join(outputDir, 'thumbnails');
ensureDirectoryExists(thumbnailsDir);
generateThumbnails(fps, inputFile, thumbnailsDir);
const storyboardImage = join(outputDir, 'storyboard.jpg');
generateStoryboard(thumbnailsDir, storyboardImage);
const vttFile = join(outputDir, 'storyboard.vtt');
generateVTT(fps, thumbnailsDir, vttFile);
cleanup(thumbnailsDir);
const elapsed = (Date.now() - startTime) / 1000;
console.log(`Process completed in ${elapsed.toFixed(2)} seconds.`);
})();
function showUsage(): never {
console.log('Usage: ts-node storyboard.ts <fps> <input_file> <output_dir>');
console.log('Example: ts-node storyboard.ts 1 example.mp4 ./output');
process.exit(1);
}
function ensureDirectoryExists(path: string) {
if (!existsSync(path)) {
mkdirSync(path, { recursive: true });
}
}
export async function generateThumbnails(fps: number, inputFile: string, thumbnailsDir: string) {
const spawn = await getNanoSpawn()
console.log('Generating thumbnails...');
try {
spawn('ffmpeg', [
'-i', inputFile,
'-vf', `fps=1/${fps},scale=384:160`,
join(thumbnailsDir, 'thumb%05d.jpg'),
], { stdio: 'inherit' });
console.log('Thumbnails generated successfully.');
} catch (err) {
console.error('Error generating thumbnails:', err);
process.exit(1);
}
}
export async function generateStoryboard(thumbnailsDir: string, storyboardImage: string) {
const spawn = await getNanoSpawn()
console.log('Creating storyboard.jpg...');
const thumbnails = readdirSync(thumbnailsDir).filter(f => f.endsWith('.jpg'));
if (thumbnails.length === 0) {
console.error('No thumbnails found. Exiting.');
process.exit(1);
}
const columns = 10;
const rows = Math.ceil(thumbnails.length / columns);
const tileFilter = `tile=${columns}x${rows}`;
try {
await spawn('ffmpeg', [
'-pattern_type', 'glob',
'-i', join(thumbnailsDir, '*.jpg'),
'-filter_complex', tileFilter,
storyboardImage,
], { stdio: 'inherit' });
console.log('Storyboard image created successfully.');
} catch (err) {
console.error('Error creating storyboard image:', err);
process.exit(1);
}
}
function generateVTT(fps: number, thumbnailsDir: string, vttFile: string) {
console.log('Generating storyboard.vtt...');
const thumbnails = readdirSync(thumbnailsDir).filter(f => f.startsWith('thumb') && f.endsWith('.jpg'));
const durationPerThumb = fps;
let vttContent = 'WEBVTT\n\n';
for (let i = 0; i < thumbnails.length; i++) {
const start = i * durationPerThumb * 1000;
const end = start + durationPerThumb * 1000;
const x = (i % 10) * 384;
const y = Math.floor(i / 10) * 160;
vttContent += `${formatDuration(start)} --> ${formatDuration(end)}\n`;
vttContent += `https://insertlinkhere.com/storyboard.jpg#xywh=${x},${y},384,160\n\n`;
}
try {
writeFileSync(vttFile, vttContent, 'utf8');
console.log('Storyboard VTT file generated successfully.');
} catch (err) {
console.error('Error writing VTT file:', err);
process.exit(1);
}
}
function cleanup(thumbnailsDir: string) {
console.log('Cleaning up temporary files...');
try {
rmSync(thumbnailsDir, { recursive: true, force: true });
} catch (err) {
console.error('Error cleaning up thumbnails:', err);
process.exit(1);
}
}
function formatDuration(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const milliseconds = ms % 1000;
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}.${pad(milliseconds, 3)}`;
}
function pad(n: number, width = 2): string {
return n.toString().padStart(width, '0');
}