You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
81 lines
2.3 KiB
81 lines
2.3 KiB
|
1 year ago
|
// convert.js
|
||
|
|
|
||
|
|
const fs = require("fs");
|
||
|
|
const path = require("path");
|
||
|
|
const { spawn, execSync } = require("child_process");
|
||
|
|
|
||
|
|
// Ensure output folder exists
|
||
|
|
if (!fs.existsSync("output")) fs.mkdirSync("output");
|
||
|
|
|
||
|
|
// Step 1: Search for files
|
||
|
|
const inputDir = "inputs";
|
||
|
|
const files = fs.readdirSync(inputDir);
|
||
|
|
|
||
|
|
const audioFile = files.find(f => f.toLowerCase().endsWith(".wav"));
|
||
|
|
const imageFile = files.find(f => f.toLowerCase().match(/\.(jpg|jpeg|svg|png)$/));
|
||
|
|
|
||
|
|
if (!audioFile || !imageFile) {
|
||
|
|
console.error("❌ Missing .wav and/or .jpg/.svg file in inputs/");
|
||
|
|
process.exit(1);
|
||
|
|
}
|
||
|
|
|
||
|
|
const audioPath = path.join(inputDir, audioFile);
|
||
|
|
const imagePath = path.join(inputDir, imageFile);
|
||
|
|
const outputName = `output-${Date.now() + Math.random() * 1000}.mp4`;
|
||
|
|
const outputPath = path.join("output", outputName);
|
||
|
|
|
||
|
|
// Step 2: Get audio duration
|
||
|
|
let durationSec = 0;
|
||
|
|
try {
|
||
|
|
const durationStr = execSync(
|
||
|
|
`ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${audioPath}"`
|
||
|
|
).toString().trim();
|
||
|
|
durationSec = parseFloat(durationStr);
|
||
|
|
console.log(`🎧 Audio duration: ${durationSec.toFixed(2)} sec`);
|
||
|
|
} catch (err) {
|
||
|
|
console.error("❌ Could not determine audio duration");
|
||
|
|
process.exit(1);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Step 3: Convert using ffmpeg
|
||
|
|
const args = [
|
||
|
|
"-loop", "1",
|
||
|
|
"-i", imagePath,
|
||
|
|
"-i", audioPath,
|
||
|
|
"-c:v", "libx264",
|
||
|
|
"-preset", "ultrafast",
|
||
|
|
"-tune", "stillimage",
|
||
|
|
"-crf", "23",
|
||
|
|
"-c:a", "aac",
|
||
|
|
"-shortest",
|
||
|
|
"-pix_fmt", "yuv420p",
|
||
|
|
"-movflags", "+faststart",
|
||
|
|
outputPath
|
||
|
|
];
|
||
|
|
|
||
|
|
console.log(`🚀 Starting conversion: ${path.basename(imageFile)} + ${path.basename(audioFile)} → ${outputName}`);
|
||
|
|
const ffmpeg = spawn("ffmpeg", args);
|
||
|
|
|
||
|
|
ffmpeg.stderr.on("data", (data) => {
|
||
|
|
const line = data.toString().trim();
|
||
|
|
const timeMatch = line.match(/time=(\d+):(\d+):([\d.]+)/);
|
||
|
|
|
||
|
|
if (timeMatch) {
|
||
|
|
const [, h, m, s] = timeMatch;
|
||
|
|
const timeSec = parseInt(h) * 3600 + parseInt(m) * 60 + parseFloat(s);
|
||
|
|
const percent = Math.min(100, ((timeSec / durationSec) * 100).toFixed(1));
|
||
|
|
console.log(`${line} | 📊 ${percent}%`);
|
||
|
|
} else {
|
||
|
|
console.log(line);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
ffmpeg.on("exit", (code) => {
|
||
|
|
if (code !== 0) {
|
||
|
|
console.error(`❌ FFmpeg exited with code ${code}`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log(`✅ Conversion complete! Saved to: ${outputPath}`);
|
||
|
|
});
|