Replace entire repo content with correct code from /root/shahikitchen-website/
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
#!/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();
|
||||
Reference in New Issue
Block a user