← back to Animate Museum Posts
src/verifyVideo.js
153 lines
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const VERIFICATION_DIR = path.join(__dirname, '..', 'verification');
// Ensure verification directory exists
if (!fs.existsSync(VERIFICATION_DIR)) fs.mkdirSync(VERIFICATION_DIR, { recursive: true });
/**
* Verify video has actual motion/animation
* Returns motion score and verification result
*/
export async function verifyVideoMotion(videoPath) {
console.log(`Verifying video motion: ${path.basename(videoPath)}`);
if (!fs.existsSync(videoPath)) {
return { verified: false, error: 'Video file not found', motionScore: 0 };
}
const timestamp = Date.now();
const framesDir = path.join(VERIFICATION_DIR, `frames_${timestamp}`);
fs.mkdirSync(framesDir, { recursive: true });
try {
// Extract frames at different timestamps
const frameCount = 5;
for (let i = 0; i < frameCount; i++) {
const time = (i + 1) * 0.5; // Extract at 0.5s, 1s, 1.5s, 2s, 2.5s
execSync(`ffmpeg -y -ss ${time} -i "${videoPath}" -vframes 1 "${framesDir}/frame_${i}.jpg" 2>/dev/null`, { stdio: 'pipe' });
}
// Compare consecutive frames using ImageMagick
let totalDiff = 0;
let comparisons = 0;
for (let i = 0; i < frameCount - 1; i++) {
const frame1 = `${framesDir}/frame_${i}.jpg`;
const frame2 = `${framesDir}/frame_${i + 1}.jpg`;
if (fs.existsSync(frame1) && fs.existsSync(frame2)) {
try {
// Get difference percentage using ImageMagick compare
const result = execSync(`compare -metric RMSE "${frame1}" "${frame2}" null: 2>&1 || true`, { encoding: 'utf8' });
const match = result.match(/\(([\d.]+)\)/);
if (match) {
totalDiff += parseFloat(match[1]) * 100;
comparisons++;
}
} catch (e) {
// ImageMagick returns non-zero exit on diff, parse output anyway
const match = e.stdout?.toString().match(/\(([\d.]+)\)/) || e.message.match(/\(([\d.]+)\)/);
if (match) {
totalDiff += parseFloat(match[1]) * 100;
comparisons++;
}
}
}
}
// Cleanup frames
fs.rmSync(framesDir, { recursive: true, force: true });
const avgDiff = comparisons > 0 ? totalDiff / comparisons : 0;
// Motion thresholds:
// < 1%: No motion (static image)
// 1-5%: Subtle motion (panning/zoom only)
// 5-15%: Moderate motion (good animation)
// > 15%: High motion (morphing/dramatic changes)
const verified = avgDiff > 3; // At least 3% difference = animation present
const motionLevel = avgDiff < 1 ? 'none' : avgDiff < 5 ? 'subtle' : avgDiff < 15 ? 'moderate' : 'high';
console.log(` Motion score: ${avgDiff.toFixed(2)}% (${motionLevel})`);
return {
verified,
motionScore: avgDiff,
motionLevel,
message: verified
? `Video has ${motionLevel} motion (${avgDiff.toFixed(1)}% frame difference)`
: `Video appears static or has minimal motion (${avgDiff.toFixed(1)}% frame difference)`
};
} catch (error) {
// Cleanup on error
try { fs.rmSync(framesDir, { recursive: true, force: true }); } catch (e) {}
console.error(` Verification error: ${error.message}`);
return { verified: false, error: error.message, motionScore: 0 };
}
}
/**
* Verify video duration is within expected range
*/
export function verifyVideoDuration(videoPath, minSeconds = 8, maxSeconds = 20) {
try {
const result = execSync(`ffprobe -v error -show_entries format=duration -of csv=p=0 "${videoPath}"`, { encoding: 'utf8' });
const duration = parseFloat(result.trim());
const verified = duration >= minSeconds && duration <= maxSeconds;
return {
verified,
duration,
message: verified
? `Duration OK: ${duration.toFixed(1)}s`
: `Duration out of range: ${duration.toFixed(1)}s (expected ${minSeconds}-${maxSeconds}s)`
};
} catch (error) {
return { verified: false, duration: 0, error: error.message };
}
}
/**
* Full video verification
*/
export async function verifyVideo(videoPath) {
console.log(`\n=== Verifying: ${path.basename(videoPath)} ===`);
const motionResult = await verifyVideoMotion(videoPath);
const durationResult = verifyVideoDuration(videoPath);
const allVerified = motionResult.verified && durationResult.verified;
console.log(` Motion: ${motionResult.message}`);
console.log(` Duration: ${durationResult.message}`);
console.log(` Overall: ${allVerified ? 'VERIFIED' : 'FAILED'}`);
return {
verified: allVerified,
motion: motionResult,
duration: durationResult
};
}
// Test if run directly
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const testVideo = process.argv[2];
if (testVideo) {
verifyVideo(testVideo)
.then(r => console.log('\nResult:', JSON.stringify(r, null, 2)))
.catch(e => console.error('Error:', e));
} else {
console.log('Usage: node verifyVideo.js <video-path>');
}
}