#!/usr/bin/env node /** * Compress sweets modal videos → uniform 640×480 (4:3), ≤ 200 KB, no audio. * Usage: * node scripts/optimize-sweets-videos.mjs * node scripts/optimize-sweets-videos.mjs public/videos/badam-barfi.mp4 */ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { spawnSync } from 'child_process'; import ffmpegPath from 'ffmpeg-static'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.join(__dirname, '..'); const VIDEOS_DIR = path.join(ROOT, 'public/videos'); const MENU_DATA = path.join(ROOT, 'infrastructure/menu/static-menu-data.ts'); const REPORT_PATH = path.join(__dirname, 'sweets-videos-optimize-report.json'); const MAX_BYTES = 200 * 1024; const TARGET_WIDTH = 640; const TARGET_HEIGHT = 480; /** Sweets dish ids with video fields in menu data */ function getSweetsVideoFiles() { const content = fs.readFileSync(MENU_DATA, 'utf8'); const sweetsIdx = content.indexOf('id: "sweets"'); const itemsStart = content.indexOf('items: [', sweetsIdx); let depth = 0; let itemsEnd = -1; for (let i = itemsStart + 7; i < content.length; i++) { if (content[i] === '[') depth++; else if (content[i] === ']') { depth--; if (depth === 0) { itemsEnd = i; break; } } } const block = content.slice(itemsStart, itemsEnd + 1); return [...block.matchAll(/video:\s*"([^"]+\.mp4)"/g)].map((m) => m[1]); } function compressVideo(inputPath, outputPath, { crf, fps, width, height }) { const vf = `scale=${width}:${height}:force_original_aspect_ratio=increase,crop=${width}:${height},fps=${fps}`; const result = spawnSync( ffmpegPath, [ '-y', '-i', inputPath, '-an', '-vf', vf, '-c:v', 'libx264', '-preset', 'medium', '-profile:v', 'baseline', '-level', '3.0', '-pix_fmt', 'yuv420p', '-movflags', '+faststart', '-crf', String(crf), outputPath, ], { stdio: 'pipe' }, ); return result.status === 0 && fs.existsSync(outputPath); } function optimizeOne(inputPath) { if (!fs.existsSync(inputPath)) return { skipped: true, reason: 'missing' }; const temp = `${inputPath}.opt.tmp.mp4`; const attempts = [ { crf: 34, fps: 24, width: TARGET_WIDTH, height: TARGET_HEIGHT }, { crf: 36, fps: 20, width: TARGET_WIDTH, height: TARGET_HEIGHT }, { crf: 38, fps: 18, width: TARGET_WIDTH, height: TARGET_HEIGHT }, { crf: 40, fps: 15, width: 560, height: 420 }, { crf: 42, fps: 12, width: 480, height: 360 }, { crf: 44, fps: 10, width: 400, height: 300 }, ]; let best = null; for (const opts of attempts) { if (fs.existsSync(temp)) fs.unlinkSync(temp); if (!compressVideo(inputPath, temp, opts)) continue; const bytes = fs.statSync(temp).size; const entry = { ...opts, bytes, kb: Number((bytes / 1024).toFixed(1)) }; if (bytes <= MAX_BYTES) { fs.renameSync(temp, inputPath); return { ok: true, ...entry, warning: null }; } if (!best || bytes < best.bytes) best = entry; } if (best && fs.existsSync(temp)) { fs.renameSync(temp, inputPath); return { ok: true, ...best, warning: 'exceeds-200kb' }; } if (fs.existsSync(temp)) fs.unlinkSync(temp); return { ok: false, reason: 'encode-failed' }; } function main() { if (!ffmpegPath) throw new Error('ffmpeg-static not available'); const arg = process.argv[2]; const files = arg ? [path.basename(arg).endsWith('.mp4') ? path.basename(arg) : `${arg}.mp4`] : getSweetsVideoFiles(); console.log('Shahi Kitchen — sweets video optimize'); console.log(`Target: ${TARGET_WIDTH}×${TARGET_HEIGHT}, ≤ ${MAX_BYTES / 1024} KB\n`); const results = []; for (const file of files) { const fullPath = path.join(VIDEOS_DIR, file); const before = fs.existsSync(fullPath) ? fs.statSync(fullPath).size : 0; const r = optimizeOne(fullPath); if (r.skipped) { console.log(`⚠ skip ${file} (${r.reason})`); results.push({ file, skipped: true, reason: r.reason }); continue; } if (!r.ok) { console.log(`✗ ${file} (${r.reason})`); results.push({ file, ok: false, reason: r.reason }); continue; } const after = fs.statSync(fullPath).size; const warn = r.warning ? ' ⚠' : ''; console.log( `✓ ${file}: ${(before / 1024).toFixed(0)} KB → ${(after / 1024).toFixed(1)} KB ` + `(${r.width}×${r.height}, crf ${r.crf}, ${r.fps}fps)${warn}`, ); results.push({ file, ok: true, beforeKb: Number((before / 1024).toFixed(1)), afterKb: Number((after / 1024).toFixed(1)), width: r.width, height: r.height, crf: r.crf, fps: r.fps, warning: r.warning, }); } fs.writeFileSync(REPORT_PATH, JSON.stringify({ generatedAt: new Date().toISOString(), results }, null, 2)); console.log(`\nReport: ${REPORT_PATH}`); } main();