Skip to content

Commit b332120

Browse files
committed
Podcasts are now in mp3 instead of wav
1 parent 8ae4adb commit b332120

26 files changed

+125
-39
lines changed

scripts/generate-podcast.js

Lines changed: 101 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import {
3636
} from "fs";
3737
import { join, relative, dirname, basename, extname } from "path";
3838
import { fileURLToPath } from "url";
39-
import { spawn } from "child_process";
39+
import { spawn, spawnSync } from "child_process";
4040
import * as readline from "readline";
4141

4242
// ES module __dirname equivalent
@@ -59,6 +59,44 @@ const API_KEY =
5959
process.env.GEMINI_API_KEY ||
6060
process.env.GCP_API_KEY;
6161

62+
/**
63+
* Check if ffmpeg is available with libmp3lame support
64+
*/
65+
function checkFfmpegAvailable() {
66+
const result = spawnSync("ffmpeg", ["-version"], {
67+
encoding: "utf-8",
68+
stdio: ["pipe", "pipe", "pipe"],
69+
});
70+
71+
if (result.error || result.status !== 0) {
72+
console.error(`
73+
Error: ffmpeg is required for MP3 conversion but was not found.
74+
75+
Install ffmpeg with:
76+
brew install ffmpeg
77+
78+
Then run this script again.
79+
`);
80+
process.exit(1);
81+
}
82+
83+
// Check for libmp3lame support
84+
const output = result.stdout || "";
85+
if (!output.includes("--enable-libmp3lame")) {
86+
console.error(`
87+
Error: ffmpeg is installed but lacks libmp3lame support for MP3 encoding.
88+
89+
Reinstall ffmpeg with MP3 support:
90+
brew reinstall ffmpeg
91+
92+
Then run this script again.
93+
`);
94+
process.exit(1);
95+
}
96+
97+
return true;
98+
}
99+
62100
// Parse command-line arguments
63101
function parseArgs() {
64102
const args = process.argv.slice(2);
@@ -1478,6 +1516,48 @@ async function generateAudio(dialogue, outputPath, genAI) {
14781516
};
14791517
}
14801518

1519+
/**
1520+
* Convert WAV file to MP3 using ffmpeg with high-quality settings
1521+
*/
1522+
async function convertWavToMp3(wavPath, mp3Path) {
1523+
return new Promise((resolve, reject) => {
1524+
const ffmpeg = spawn("ffmpeg", [
1525+
"-y", // Overwrite output file
1526+
"-i",
1527+
wavPath,
1528+
"-codec:a",
1529+
"libmp3lame",
1530+
"-q:a",
1531+
"2", // VBR quality 2 (~190 kbps) - high quality for voice
1532+
mp3Path,
1533+
]);
1534+
1535+
let stderr = "";
1536+
ffmpeg.stderr.on("data", (data) => {
1537+
stderr += data.toString();
1538+
});
1539+
1540+
ffmpeg.on("close", (code) => {
1541+
if (code === 0) {
1542+
// Delete the intermediate WAV file
1543+
unlinkSync(wavPath);
1544+
1545+
const stats = statSync(mp3Path);
1546+
console.log(
1547+
` 🔊 Converted to MP3: ${(stats.size / 1024 / 1024).toFixed(2)} MB`,
1548+
);
1549+
resolve({ size: stats.size, format: "audio/mp3" });
1550+
} else {
1551+
reject(new Error(`ffmpeg failed with code ${code}: ${stderr}`));
1552+
}
1553+
});
1554+
1555+
ffmpeg.on("error", (err) => {
1556+
reject(new Error(`Failed to spawn ffmpeg: ${err.message}`));
1557+
});
1558+
});
1559+
}
1560+
14811561
// ============================================================================
14821562
// FILE DISCOVERY AND SELECTION
14831563
// ============================================================================
@@ -1719,32 +1799,37 @@ async function generateAudioFromScript(scriptPath, audioManifest, genAI) {
17191799
` 📊 Estimated tokens: ${frontmatter.tokenCount || "unknown"}`,
17201800
);
17211801

1722-
// Determine audio output path
1802+
// Determine audio output paths
17231803
const relativeDir = dirname(relativePath);
1724-
const outputFileName = `${fileName}.wav`;
1725-
const outputPath = join(AUDIO_OUTPUT_DIR, relativeDir, outputFileName);
1804+
const wavFileName = `${fileName}.wav`;
1805+
const mp3FileName = `${fileName}.mp3`;
1806+
const wavPath = join(AUDIO_OUTPUT_DIR, relativeDir, wavFileName);
1807+
const mp3Path = join(AUDIO_OUTPUT_DIR, relativeDir, mp3FileName);
1808+
1809+
// Generate WAV audio first
1810+
const wavInfo = await generateAudio(dialog, wavPath, genAI);
17261811

1727-
// Generate audio
1728-
const audioInfo = await generateAudio(dialog, outputPath, genAI);
1812+
// Convert WAV to MP3
1813+
const mp3Info = await convertWavToMp3(wavPath, mp3Path);
17291814

17301815
// Update manifest using the source doc path as key
1731-
const audioUrl = `/audio/${join(relativeDir, outputFileName)}`;
1816+
const audioUrl = `/audio/${join(relativeDir, mp3FileName)}`;
17321817
audioManifest[frontmatter.source] = {
17331818
audioUrl,
1734-
size: audioInfo.size,
1735-
format: audioInfo.format,
1736-
tokenCount: audioInfo.tokenCount,
1737-
chunks: audioInfo.chunks,
1819+
size: mp3Info.size,
1820+
format: mp3Info.format,
1821+
tokenCount: wavInfo.tokenCount,
1822+
chunks: wavInfo.chunks,
17381823
generatedAt: new Date().toISOString(),
17391824
scriptSource: relativePath,
17401825
};
17411826

17421827
console.log(` ✅ Generated: ${audioUrl}`);
17431828
console.log(
1744-
` 📊 Audio size: ${(audioInfo.size / 1024 / 1024).toFixed(2)} MB`,
1829+
` 📊 Audio size: ${(mp3Info.size / 1024 / 1024).toFixed(2)} MB`,
17451830
);
1746-
if (audioInfo.chunks > 1) {
1747-
console.log(` 🧩 Chunks: ${audioInfo.chunks}`);
1831+
if (wavInfo.chunks > 1) {
1832+
console.log(` 🧩 Chunks: ${wavInfo.chunks}`);
17481833
}
17491834
} catch (error) {
17501835
console.error(` ❌ Error: ${error.message}`);
@@ -1810,7 +1895,7 @@ async function main() {
18101895
console.log(`📋 Pipeline: ${config.pipeline}`);
18111896
console.log(`📋 Mode: ${config.mode}`);
18121897

1813-
// Initialize Gemini API if needed for audio
1898+
// Initialize Gemini API and check ffmpeg if needed for audio
18141899
let genAI = null;
18151900
if (config.pipeline !== "script-only") {
18161901
if (!API_KEY) {
@@ -1820,6 +1905,7 @@ async function main() {
18201905
);
18211906
process.exit(1);
18221907
}
1908+
checkFfmpegAvailable();
18231909
genAI = new GoogleGenerativeAI(API_KEY);
18241910
}
18251911

0 commit comments

Comments
 (0)