Files
shahikitchen-prod/scripts/optimize-sweets-videos.mjs

159 lines
4.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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();