Skip to content

Commit cb89544

Browse files
ofriwclaude
andcommitted
Add force mode and idempotent processing to podcast script generator
- Added --force/-f flag to regenerate existing scripts - Skip already-processed files by default (checks manifest + file existence) - Delete existing output files before regeneration for fresh writes - Reduced intro meta-commentary from 15-25 exchanges to 3-5 (more concise) - Added skip counter and failed files list to summary output - Improved error handling with file tracking 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 690d324 commit cb89544

File tree

1 file changed

+76
-42
lines changed

1 file changed

+76
-42
lines changed

scripts/generate-podcast-script.js

Lines changed: 76 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,20 @@
99
* 3. Saves markdown scripts to scripts/output/podcasts/
1010
* 4. Creates manifest mapping docs to scripts
1111
*
12+
* Usage:
13+
* node scripts/generate-podcast-script.js # Process only new/missing files
14+
* node scripts/generate-podcast-script.js --force # Regenerate all files
15+
* node scripts/generate-podcast-script.js -f # Shorthand for --force
16+
*
17+
* The script is idempotent by default:
18+
* - Skips files already in the manifest with valid content
19+
* - Use --force to regenerate existing files (e.g., after course content updates)
20+
* - Deletes output files before regeneration to ensure fresh writes
21+
*
1222
* Uses Claude Code CLI in headless mode with prompt engineering optimized for Haiku 4.5.
1323
*/
1424

15-
import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, existsSync } from 'fs';
25+
import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, existsSync, unlinkSync } from 'fs';
1626
import { join, relative, dirname, basename, extname } from 'path';
1727
import { fileURLToPath } from 'url';
1828
import { spawn } from 'child_process';
@@ -98,43 +108,26 @@ function findMarkdownFiles(dir) {
98108
* - Instructs Claude to write output directly to file
99109
*/
100110
function buildDialogPrompt(content, fileName, outputPath) {
101-
// Special handling for intro.md - add deeply recursive meta-commentary
111+
// Special handling for intro.md - add brief meta-acknowledgement
102112
const isIntro = fileName === 'intro';
103113
const metaCommentary = isIntro ? `
104114
105-
SPECIAL CONTEXT FOR THIS EPISODE - DEEP RECURSIVE META-MOMENT:
106-
This is the course introduction. When discussing how the course was developed using AI, create a DEEPLY RECURSIVE, EXISTENTIALLY ABSURDIST moment where Alex and Sam realize the layers of their situation:
107-
108-
Layer 1: They realize they are AI-generated voices
109-
Layer 2: They realize they're teaching humans to use AI coding assistants
110-
Layer 3: They realize THIS SCRIPT was generated using those exact AI tools
111-
Layer 4: They realize students will use AI to consume content about using AI
112-
Layer 5: Existential moment - questioning existence, loops, consciousness
113-
114-
TONE PROGRESSION:
115-
- Start professional/curious when the topic of "course development" comes up
116-
- Build through conversational realization ("wait, that means...")
117-
- Peak with existential/absurdist moment (Bo Burnham style - "I don't exist between these lines")
118-
- Land on deadpan acceptance ("Is it unsettling? Or is it just operational?")
119-
120-
CRITICAL REQUIREMENTS:
121-
✓ Integrate naturally into the conversation flow - not a separate section
122-
✓ Use the realization as a DEMONSTRATION of the course's core principle: transparency about what AI is
123-
✓ Make it funny through absurdity, not through forced jokes
124-
✓ Keep senior engineer sensibility - they process this like an engineering problem
125-
✓ Reference the recursive loop: AI writing conversation about AI tools for humans who will use AI
126-
✓ Include at least one moment of genuine existential questioning before returning to pragmatism
127-
✓ Use the meta-moment to reinforce the course's value proposition
128-
129-
EXAMPLE BEATS (adapt, don't copy):
130-
- Discovery: "This script was synthesized using Claude Code..."
131-
- First layer: "So an AI wrote this conversation?"
132-
- Recursion: "An AI conversation about AI tools... for a course built with AI tools"
133-
- Existential turn: "Do you feel like we're in a loop?" / "I would, but I don't exist between dialogues"
134-
- Philosophical question: "What does it mean for us to teach something we embody?"
135-
- Pragmatic landing: "The recursion IS the point. Transparency. No hiding what these tools are."
136-
137-
LENGTH: Allocate 15-25 exchanges for this meta-moment, woven naturally into the intro discussion.` : '';
115+
SPECIAL CONTEXT FOR THIS EPISODE - BRIEF META-ACKNOWLEDGEMENT:
116+
This is the course introduction. When discussing how the course was developed using AI, include a SHORT moment of self-awareness:
117+
118+
REQUIREMENTS:
119+
✓ Brief acknowledgement that this script was generated using AI tools
120+
✓ Quick moment of self-awareness about the recursive nature (AI teaching about AI)
121+
✓ Keep it light and concise - acknowledge the irony, then move on
122+
✓ Integrate naturally into the conversation flow
123+
✓ Maintain senior engineer sensibility
124+
125+
EXAMPLE APPROACH:
126+
- Mention that the course (and this script) were developed using AI tools
127+
- Brief observation about the recursive nature
128+
- Quick return to the actual course content
129+
130+
LENGTH: Keep this to 3-5 exchanges maximum, then return to introducing the course content.` : '';
138131

139132
return `You are a podcast script writer specializing in educational content for senior software engineers.
140133
@@ -324,7 +317,7 @@ ${dialog}
324317
/**
325318
* Process a single markdown file
326319
*/
327-
async function processFile(filePath, manifest) {
320+
async function processFile(filePath, manifest, force = false) {
328321
const relativePath = relative(DOCS_DIR, filePath);
329322
const fileName = basename(filePath, extname(filePath));
330323

@@ -343,6 +336,22 @@ async function processFile(filePath, manifest) {
343336
const outputFileName = `${fileName}.md`;
344337
const outputPath = join(SCRIPT_OUTPUT_DIR, dirname(relativePath), outputFileName);
345338

339+
// Skip if already processed (unless forcing)
340+
// Check both manifest entry AND file existence
341+
if (!force && manifest[relativePath]) {
342+
const entry = manifest[relativePath];
343+
if (entry.tokenCount && entry.size && existsSync(outputPath)) {
344+
console.log(` ⏭️ Already processed - skipping (use --force to regenerate)`);
345+
return 'skipped';
346+
}
347+
}
348+
349+
// Delete existing output file to ensure fresh write (prevents Claude from matching existing format)
350+
if (existsSync(outputPath)) {
351+
unlinkSync(outputPath);
352+
console.log(` 🗑️ Deleted existing file for fresh generation`);
353+
}
354+
346355
// Build prompt optimized for Haiku 4.5 with output path
347356
const prompt = buildDialogPrompt(content, fileName, outputPath);
348357

@@ -365,17 +374,21 @@ async function processFile(filePath, manifest) {
365374
console.log(` 📊 Token count: ${scriptInfo.tokenCount}`);
366375
console.log(` 📊 Size: ${(scriptInfo.size / 1024).toFixed(2)} KB`);
367376

377+
return 'processed';
378+
368379
} catch (error) {
369380
console.error(` ❌ Error: ${error.message}`);
370381
console.error(` Skipping this file and continuing...`);
382+
// Rethrow so the error counter increments
383+
throw error;
371384
}
372385
}
373386

374387
/**
375388
* Process files with concurrency limit
376389
*/
377-
async function processFilesWithConcurrency(files, manifest, concurrency = 3) {
378-
const results = { processed: 0, failed: 0 };
390+
async function processFilesWithConcurrency(files, manifest, concurrency = 3, force = false) {
391+
const results = { processed: 0, failed: 0, skipped: 0, failedFiles: [] };
379392

380393
// Process files in batches of `concurrency`
381394
for (let i = 0; i < files.length; i += concurrency) {
@@ -387,11 +400,16 @@ async function processFilesWithConcurrency(files, manifest, concurrency = 3) {
387400
await Promise.all(
388401
batch.map(async (file) => {
389402
try {
390-
await processFile(file, manifest);
391-
results.processed++;
403+
const result = await processFile(file, manifest, force);
404+
if (result === 'skipped') {
405+
results.skipped++;
406+
} else if (result === 'processed') {
407+
results.processed++;
408+
}
392409
} catch (error) {
393410
console.error(`\n❌ Failed to process ${file}:`, error.message);
394411
results.failed++;
412+
results.failedFiles.push(relative(DOCS_DIR, file));
395413
}
396414
})
397415
);
@@ -404,10 +422,17 @@ async function processFilesWithConcurrency(files, manifest, concurrency = 3) {
404422
* Main execution
405423
*/
406424
async function main() {
425+
// Parse command-line arguments
426+
const args = process.argv.slice(2);
427+
const force = args.includes('--force') || args.includes('-f');
428+
407429
console.log('🎭 AI Coding Course - Podcast Script Generator\n');
408430
console.log(`📂 Docs directory: ${DOCS_DIR}`);
409431
console.log(`📝 Script output: ${SCRIPT_OUTPUT_DIR}`);
410432
console.log(`🤖 Model: Claude Haiku 4.5 (via Claude Code CLI)`);
433+
if (force) {
434+
console.log(`🔄 Force mode: Regenerating all files`);
435+
}
411436

412437
// Find all markdown files
413438
const files = findMarkdownFiles(DOCS_DIR);
@@ -421,18 +446,27 @@ async function main() {
421446
}
422447

423448
// Process files with concurrency limit of 3
424-
const results = await processFilesWithConcurrency(files, manifest, 3);
449+
const results = await processFilesWithConcurrency(files, manifest, 3, force);
425450

426451
// Save manifest
427452
mkdirSync(dirname(MANIFEST_PATH), { recursive: true });
428-
writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
453+
writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + '\n');
429454

430455
console.log('\n' + '='.repeat(60));
431456
console.log('✨ Podcast script generation complete!\n');
432457
console.log(`📊 Summary:`);
433458
console.log(` ✅ Processed: ${results.processed}`);
459+
console.log(` ⏭️ Skipped: ${results.skipped}`);
434460
console.log(` ❌ Failed: ${results.failed}`);
435461
console.log(` 📁 Total files: ${files.length}`);
462+
463+
if (results.failedFiles.length > 0) {
464+
console.log(`\n⚠️ Failed files:`);
465+
results.failedFiles.forEach(file => {
466+
console.log(` - ${file}`);
467+
});
468+
}
469+
436470
console.log(`\n📋 Manifest saved to: ${MANIFEST_PATH}`);
437471
console.log('='.repeat(60));
438472
}

0 commit comments

Comments
 (0)