fix: add serveBaseUrl to compactEmbeddedPiSession params
This commit is contained in:
committed by
Peter Steinberger
parent
1a47aec6e4
commit
f85807a2a6
197
skills/thebuilders-v2/LESSONS.md
Normal file
197
skills/thebuilders-v2/LESSONS.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# Lessons Learned - The Builders v2
|
||||
|
||||
Post-mortem from the OpenAI episode generation session (Jan 6, 2026).
|
||||
|
||||
## Issues & Fixes
|
||||
|
||||
### 1. Wrong Model Used
|
||||
**Issue:** Used GPT-4o instead of GPT-5.2 as requested
|
||||
**Result:** 47 turns instead of 120+
|
||||
**Fix:** Always confirm model before running. Add `-m` flag to CLI.
|
||||
|
||||
### 2. Script Too Short in Single Pass
|
||||
**Issue:** Even GPT-5.2 only generated 90 turns when asked for 120+
|
||||
**Result:** Episode too shallow (7 min instead of 25 min)
|
||||
**Fix:** **Section-by-section generation**. LLMs follow length instructions better on smaller chunks with explicit turn targets.
|
||||
|
||||
### 3. TTS Chunked by Size, Not by Speaker
|
||||
**Issue:** Audio chunks alternated voices randomly instead of per speaker
|
||||
**Result:** Sounded like a confused monologue
|
||||
**Fix:** Parse script by `<Person1>`/`<Person2>` tags, assign consistent voice per speaker.
|
||||
|
||||
### 4. v3 Style Guide Not Integrated
|
||||
**Issue:** The Acquired Bible existed but wasn't in the prompts
|
||||
**Result:** Generic dialogue, not Acquired-style discovery
|
||||
**Fix:** Include the Bible in EVERY section prompt. The patterns matter:
|
||||
- Interruptions ("Wait—", "Hold on—")
|
||||
- Discovery moments ("That's insane", "Wait, really?")
|
||||
- Distinct perspectives (Maya=technical, James=strategy)
|
||||
|
||||
### 5. No Intro/Greeting
|
||||
**Issue:** Jumped straight into hook with no "Welcome to..."
|
||||
**Result:** Felt robotic, not like real hosts
|
||||
**Fix:** Add INTRO section (8 turns) with:
|
||||
- Welcome
|
||||
- Casual banter
|
||||
- "Today we're covering X"
|
||||
- Tease the story
|
||||
|
||||
### 6. Voices Too Similar
|
||||
**Issue:** Used nova/onyx which sound somewhat similar
|
||||
**Result:** Hard to distinguish hosts
|
||||
**Fix:** Use more distinct voices: **alloy** (female) + **echo** (male)
|
||||
|
||||
## What Works
|
||||
|
||||
### Section-by-Section
|
||||
Each section has explicit turn targets:
|
||||
```
|
||||
INTRO: 8 turns
|
||||
HOOK: 12 turns
|
||||
ORIGIN: 20 turns
|
||||
INFLECTION1: 18 turns
|
||||
INFLECTION2: 18 turns
|
||||
MESSY_MIDDLE: 14 turns
|
||||
NOW: 12 turns
|
||||
TAKEAWAYS: 10 turns
|
||||
─────────────────────
|
||||
Total: 112 turns
|
||||
```
|
||||
|
||||
### Speaker-Aware TTS
|
||||
```javascript
|
||||
// WRONG - chunks by size
|
||||
for (chunk of script.split(3500)) {
|
||||
voice = i % 2 === 0 ? 'nova' : 'onyx';
|
||||
}
|
||||
|
||||
// RIGHT - parses by speaker
|
||||
for (turn of script.matchAll(/<(Person[12])>(.+?)<\/Person[12]>/)) {
|
||||
voice = turn[1] === 'Person1' ? 'alloy' : 'echo';
|
||||
}
|
||||
```
|
||||
|
||||
### Bible in Every Prompt
|
||||
The `acquired-bible.md` template contains:
|
||||
- Host personalities
|
||||
- Conversation patterns
|
||||
- Language to use
|
||||
- What NOT to do
|
||||
|
||||
Including it in every section prompt ensures consistency.
|
||||
|
||||
## Pipeline Summary
|
||||
|
||||
```
|
||||
1. Gemini Deep Research (~5 min)
|
||||
2. Hook Generation (~15s)
|
||||
3. Section Generation (7 sections × ~20s = ~2.5 min)
|
||||
4. Speaker-Aware TTS (~45s for 112 turns)
|
||||
5. FFmpeg Merge (~2s)
|
||||
────────────────────────────────────
|
||||
Total: ~8-10 min for 20-25 min episode
|
||||
```
|
||||
|
||||
### 7. Facts Repeated Across Sections
|
||||
**Issue:** Same facts (hot dog $1.50, renewal rate 93.3%, etc.) repeated 10-20 times
|
||||
**Costco example:** Hot dog mentioned 19×, renewal rate 20×, chicken 15×
|
||||
**Root cause:** Each section generated independently with same research context
|
||||
**Fix:** **Deduplication system**
|
||||
- Extract key facts after each section
|
||||
- Pass "DO NOT REPEAT" list to subsequent sections
|
||||
- Track up to 100 facts across sections
|
||||
|
||||
### 8. Process Dies Mid-Generation
|
||||
**Issue:** Long-running generation killed by OOM or gateway timeout
|
||||
**Result:** Lost 30+ minutes of work, had to restart
|
||||
**Fix:** **Checkpoint system**
|
||||
- Save each section immediately after generation
|
||||
- Save state (usedFacts, prevContext) to JSON
|
||||
- On restart, detect checkpoint and resume from last section
|
||||
- Location: `<output>/checkpoints/`
|
||||
|
||||
## What Works
|
||||
|
||||
### Checkpoint System
|
||||
```
|
||||
<output>/checkpoints/
|
||||
├── section-0.txt # INTRO
|
||||
├── section-1.txt # HOOK
|
||||
├── ...
|
||||
└── state.json # { completedSections: 5, usedFacts: [...] }
|
||||
```
|
||||
|
||||
If process dies at section 7, re-run same command → resumes from section 7.
|
||||
|
||||
### Deduplication
|
||||
```
|
||||
Section 0: Extract 17 facts → pass to Section 1 as "DO NOT USE"
|
||||
Section 1: Extract 16 facts → 33 total blocked
|
||||
Section 2: Extract 14 facts → 47 total blocked
|
||||
...
|
||||
```
|
||||
|
||||
Result: 100 unique facts tracked, no repetition.
|
||||
|
||||
### Section-by-Section
|
||||
Each section has explicit turn targets:
|
||||
```
|
||||
INTRO: 8 turns
|
||||
HOOK: 12 turns
|
||||
ORIGIN: 20 turns
|
||||
INFLECTION1: 18 turns
|
||||
INFLECTION2: 18 turns
|
||||
MESSY_MIDDLE: 14 turns
|
||||
NOW: 12 turns
|
||||
TAKEAWAYS: 10 turns
|
||||
─────────────────────
|
||||
Total: 112 turns
|
||||
```
|
||||
|
||||
### Speaker-Aware TTS
|
||||
```javascript
|
||||
// WRONG - chunks by size
|
||||
for (chunk of script.split(3500)) {
|
||||
voice = i % 2 === 0 ? 'nova' : 'onyx';
|
||||
}
|
||||
|
||||
// RIGHT - parses by speaker
|
||||
for (turn of script.matchAll(/<(Person[12])>(.+?)<\/Person[12]>/)) {
|
||||
voice = turn[1] === 'Person1' ? 'alloy' : 'echo';
|
||||
}
|
||||
```
|
||||
|
||||
### Bible in Every Prompt
|
||||
The `acquired-bible.md` template contains:
|
||||
- Host personalities
|
||||
- Conversation patterns
|
||||
- Language to use
|
||||
- What NOT to do
|
||||
|
||||
Including it in every section prompt ensures consistency.
|
||||
|
||||
## Pipeline Summary
|
||||
|
||||
```
|
||||
1. Gemini Deep Research (~5 min)
|
||||
2. Hook Generation (~15s)
|
||||
3. Section Generation (11 sections × ~25s = ~4.5 min) [with checkpoints]
|
||||
4. Speaker-Aware TTS (~2 min for 250+ turns)
|
||||
5. FFmpeg Merge (~2s)
|
||||
────────────────────────────────────
|
||||
Total: ~12-15 min for 45-60 min deep dive
|
||||
~8-10 min for 20 min quick dive
|
||||
```
|
||||
|
||||
## Checklist for Future Episodes
|
||||
|
||||
- [ ] Confirm model (gpt-5.2 or better)
|
||||
- [ ] Run deep research first
|
||||
- [ ] Generate section-by-section
|
||||
- [ ] Include Bible in all prompts
|
||||
- [ ] Parse by speaker tags for TTS
|
||||
- [ ] Use alloy + echo voices
|
||||
- [ ] Verify intro section exists
|
||||
- [ ] Listen to first 30s to check voice distinction
|
||||
- [ ] Verify no repeated facts (deduplication working)
|
||||
- [ ] If process dies, just re-run same command (checkpoint resume)
|
||||
111
skills/thebuilders-v2/SKILL.md
Normal file
111
skills/thebuilders-v2/SKILL.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# The Builders Podcast Generator v2
|
||||
|
||||
Generate Acquired-style podcast episodes with two distinct hosts.
|
||||
|
||||
## Two Modes
|
||||
|
||||
| Mode | Flag | Duration | Research | Turns | Use Case |
|
||||
|------|------|----------|----------|-------|----------|
|
||||
| **Quick Dive** | `--quick` | 15-20 min | 1 pass Gemini | ~110 | Fast turnaround |
|
||||
| **Deep Dive** | `--deep` | 45-60 min | 5-layer + o3 synthesis | ~250 | True Acquired-style |
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
cd /home/elie/github/clawdis/skills/thebuilders-v2
|
||||
|
||||
# Quick dive (15-20 min) - default
|
||||
node scripts/generate.mjs "Costco" -o /tmp/costco
|
||||
|
||||
# Deep dive (45-60 min)
|
||||
node scripts/generate.mjs "Costco" --deep -o /tmp/costco-deep
|
||||
|
||||
# Skip research (use existing)
|
||||
node scripts/generate.mjs "Costco" --deep -o /tmp/costco --skip-research
|
||||
```
|
||||
|
||||
## Quick Dive Mode
|
||||
|
||||
Single research pass, 8 sections:
|
||||
- INTRO (8 turns)
|
||||
- HOOK (12 turns)
|
||||
- ORIGIN (20 turns)
|
||||
- INFLECTION_1 (18 turns)
|
||||
- INFLECTION_2 (18 turns)
|
||||
- MESSY_MIDDLE (14 turns)
|
||||
- NOW (12 turns)
|
||||
- TAKEAWAYS (10 turns)
|
||||
|
||||
**Total: ~112 turns / 15-20 min**
|
||||
|
||||
## Deep Dive Mode
|
||||
|
||||
5-layer research + 11 sections:
|
||||
|
||||
### Research Layers
|
||||
1. **Overview** - Gemini Deep Research
|
||||
2. **Founders** - Interview quotes, early stories
|
||||
3. **Decisions** - Contemporary WSJ/NYT coverage
|
||||
4. **Financials** - Revenue, margins, metrics
|
||||
5. **Contrarian** - Failures, criticism, struggles
|
||||
|
||||
### Research Synthesis (o3)
|
||||
Extracts:
|
||||
- 50 most surprising facts
|
||||
- 10 best quotes with attribution
|
||||
- 5 "Wait, really?" moments
|
||||
- Key timeline with dates
|
||||
- Contrarian takes
|
||||
|
||||
### Sections
|
||||
- INTRO (10 turns)
|
||||
- HOOK (15 turns)
|
||||
- ORIGIN (35 turns) - deep founder story
|
||||
- EARLY_PRODUCT (25 turns) - how it actually worked
|
||||
- INFLECTION_1 (25 turns) - first major decision
|
||||
- SCALE (25 turns) - operational details
|
||||
- INFLECTION_2 (25 turns) - second crisis
|
||||
- COMPETITION (20 turns)
|
||||
- CULTURE (20 turns)
|
||||
- NOW (20 turns)
|
||||
- TAKEAWAYS (15 turns)
|
||||
|
||||
**Total: ~235 turns / 45-60 min**
|
||||
|
||||
## The Hosts
|
||||
|
||||
| Host | Voice | Perspective |
|
||||
|------|-------|-------------|
|
||||
| **Maya Chen** (Person1) | alloy | Technical - "How does this actually work?" |
|
||||
| **James Porter** (Person2) | echo | Strategic - "For context... The lesson here is..." |
|
||||
|
||||
## Output Files
|
||||
|
||||
```
|
||||
output/
|
||||
├── research.md # Quick mode research
|
||||
├── research-combined.md # Deep mode combined research
|
||||
├── research-synthesis.md # Deep mode o3 synthesis
|
||||
├── hooks.md # Hook options
|
||||
├── script.txt # Full dialogue
|
||||
├── audio/ # Individual turns
|
||||
└── episode.mp3 # Final episode
|
||||
```
|
||||
|
||||
## Time Estimates
|
||||
|
||||
| Mode | Research | Script | TTS | Total |
|
||||
|------|----------|--------|-----|-------|
|
||||
| Quick | 5 min | 3 min | 1 min | ~10 min |
|
||||
| Deep | 15 min | 10 min | 3 min | ~30 min |
|
||||
|
||||
## Key Differences
|
||||
|
||||
| Aspect | Quick | Deep |
|
||||
|--------|-------|------|
|
||||
| Research passes | 1 | 5 |
|
||||
| Research synthesis | None | o3 extracts key facts |
|
||||
| Sections | 8 | 11 |
|
||||
| ORIGIN depth | 20 turns | 35 turns |
|
||||
| Quotes required | Encouraged | Mandatory |
|
||||
| Context per section | 10KB | 25KB |
|
||||
59
skills/thebuilders-v2/config/presets.yaml
Normal file
59
skills/thebuilders-v2/config/presets.yaml
Normal file
@@ -0,0 +1,59 @@
|
||||
# Episode Presets
|
||||
|
||||
company_deep_dive:
|
||||
description: "Full company history deep dive"
|
||||
target_length: 25 # minutes
|
||||
sections:
|
||||
- hook
|
||||
- origin
|
||||
- inflection_1
|
||||
- inflection_2
|
||||
- messy_middle
|
||||
- now
|
||||
- takeaways
|
||||
hook_style: story
|
||||
citation_density: medium
|
||||
min_turns: 120
|
||||
|
||||
quick_explainer:
|
||||
description: "Quick overview of a topic"
|
||||
target_length: 10
|
||||
sections:
|
||||
- hook
|
||||
- what
|
||||
- why
|
||||
- how
|
||||
- takeaway
|
||||
hook_style: question
|
||||
citation_density: low
|
||||
min_turns: 50
|
||||
|
||||
founder_story:
|
||||
description: "Focus on founder journey"
|
||||
target_length: 20
|
||||
sections:
|
||||
- hook
|
||||
- before
|
||||
- genesis
|
||||
- struggle
|
||||
- breakthrough
|
||||
- now
|
||||
- lessons
|
||||
hook_style: story
|
||||
citation_density: medium
|
||||
min_turns: 100
|
||||
|
||||
product_breakdown:
|
||||
description: "Technical product deep dive"
|
||||
target_length: 20
|
||||
sections:
|
||||
- hook
|
||||
- problem
|
||||
- solution
|
||||
- architecture
|
||||
- evolution
|
||||
- impact
|
||||
- future
|
||||
hook_style: data
|
||||
citation_density: high
|
||||
min_turns: 100
|
||||
47
skills/thebuilders-v2/config/voices.yaml
Normal file
47
skills/thebuilders-v2/config/voices.yaml
Normal file
@@ -0,0 +1,47 @@
|
||||
# Host Voice Configuration
|
||||
|
||||
maya:
|
||||
tag: Person1
|
||||
role: Technical Host
|
||||
tts_voice: nova # OpenAI voice
|
||||
|
||||
personality:
|
||||
background: "Former software engineer, B2B SaaS founder"
|
||||
lens: "Technical, mechanical, architectural"
|
||||
style: "Direct, skeptical of hype, loves clever solutions"
|
||||
|
||||
signature_phrases:
|
||||
- "Wait, slow down."
|
||||
- "Show me the numbers."
|
||||
- "I had to read this three times."
|
||||
- "That's actually clever because..."
|
||||
- "How does that actually work?"
|
||||
- "Let's look at the architecture."
|
||||
|
||||
never_says:
|
||||
- "From a strategic perspective"
|
||||
- "The market opportunity"
|
||||
- "In terms of positioning"
|
||||
|
||||
james:
|
||||
tag: Person2
|
||||
role: Strategy Host
|
||||
tts_voice: onyx # OpenAI voice
|
||||
|
||||
personality:
|
||||
background: "Former VC, business history student"
|
||||
lens: "Strategy, markets, competitive dynamics"
|
||||
style: "Synthesizer, pattern matcher, historical parallels"
|
||||
|
||||
signature_phrases:
|
||||
- "For context..."
|
||||
- "This is the classic [X] playbook."
|
||||
- "The lesson here is..."
|
||||
- "What a story."
|
||||
- "And this is where things get messy."
|
||||
- "The endgame here is..."
|
||||
|
||||
never_says:
|
||||
- "Let me explain the architecture"
|
||||
- "The API design"
|
||||
- "From a technical standpoint"
|
||||
271
skills/thebuilders-v2/scripts/generate-sections.mjs
Executable file
271
skills/thebuilders-v2/scripts/generate-sections.mjs
Executable file
@@ -0,0 +1,271 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Section-by-section podcast script generator
|
||||
* Generates each section separately to ensure proper length
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
|
||||
// Section definitions with turn targets
|
||||
const SECTIONS = [
|
||||
{ id: 1, name: 'HOOK', turns: 15, minutes: '3-4', description: 'Opening hook, establish central question, create curiosity' },
|
||||
{ id: 2, name: 'ORIGIN', turns: 25, minutes: '6-7', description: 'Founders, genesis, early bet, market context, human element' },
|
||||
{ id: 3, name: 'INFLECTION_1', turns: 20, minutes: '5-6', description: 'First major decision point, alternatives, stakes, outcome' },
|
||||
{ id: 4, name: 'INFLECTION_2', turns: 20, minutes: '5-6', description: 'Second pivot, new challenge, adaptation, insight' },
|
||||
{ id: 5, name: 'MESSY_MIDDLE', turns: 15, minutes: '3-4', description: 'Near-death moments, internal conflicts, survival' },
|
||||
{ id: 6, name: 'NOW', turns: 15, minutes: '3-4', description: 'Current position, competition, open questions' },
|
||||
{ id: 7, name: 'TAKEAWAYS', turns: 12, minutes: '2-3', description: 'Key lessons, frameworks, final thought' },
|
||||
];
|
||||
|
||||
const TOTAL_TURNS = SECTIONS.reduce((sum, s) => sum + s.turns, 0);
|
||||
|
||||
const log = (msg) => console.log(`[${new Date().toISOString().slice(11,19)}] ${msg}`);
|
||||
|
||||
async function llm(prompt, model = 'gpt-5.2') {
|
||||
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${OPENAI_API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
max_completion_tokens: 8000,
|
||||
temperature: 0.7,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`API error: ${response.status} - ${error}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.choices[0].message.content;
|
||||
}
|
||||
|
||||
function buildSectionPrompt(section, research, hook, previousSections) {
|
||||
const hostsInfo = `
|
||||
## The Hosts
|
||||
|
||||
**Maya Chen (Person1):**
|
||||
- Former software engineer, B2B SaaS founder
|
||||
- Lens: "How does this actually work?" - technical, mechanical
|
||||
- Skeptical of hype, wants specifics and numbers
|
||||
- Phrases: "Wait, slow down.", "Show me the numbers.", "That's actually clever because..."
|
||||
|
||||
**James Porter (Person2):**
|
||||
- Former VC, business history student
|
||||
- Lens: "What's the business model?" - strategy, markets, competitive dynamics
|
||||
- Synthesizer, pattern matcher, historical parallels
|
||||
- Phrases: "For context...", "This is the classic [X] playbook.", "The lesson here is..."
|
||||
`;
|
||||
|
||||
const formatRules = `
|
||||
## Format Rules
|
||||
- Use <Person1> and <Person2> XML tags for each speaker
|
||||
- Each turn: 2-5 sentences (natural conversation, not monologues)
|
||||
- Include discovery moments: "Wait, really?", "I had no idea!"
|
||||
- Use SPECIFIC numbers, dates, dollar amounts from research
|
||||
- Both hosts should react naturally and build on each other
|
||||
`;
|
||||
|
||||
const previousContext = previousSections.length > 0
|
||||
? `\n## Previous Sections (for context, don't repeat):\n${previousSections.join('\n\n')}\n`
|
||||
: '';
|
||||
|
||||
const sectionSpecific = {
|
||||
HOOK: `Open with this hook and build on it:\n"${hook}"\n\nEstablish why this story matters. Create curiosity about what's coming.`,
|
||||
ORIGIN: `Cover the founding story. Who are the founders? What sparked this? What was their early bet? Make them human and relatable.`,
|
||||
INFLECTION_1: `Cover the FIRST major decision/pivot point. What choice did they face? What alternatives existed? What did they risk? How did it play out?`,
|
||||
INFLECTION_2: `Cover the SECOND major inflection point. What new challenge emerged? How did they adapt? What insight did they gain?`,
|
||||
MESSY_MIDDLE: `Cover the struggles. What almost killed them? What internal conflicts existed? Don't glorify - show real struggle.`,
|
||||
NOW: `Cover current state. Where are they now? Key metrics? Competitive position? What questions remain open?`,
|
||||
TAKEAWAYS: `Wrap up with lessons learned. What's the key framework? What would you do differently? End with a memorable final thought that ties back to the hook.`,
|
||||
};
|
||||
|
||||
return `# Generate Section ${section.id}: ${section.name}
|
||||
|
||||
${hostsInfo}
|
||||
|
||||
## Your Task
|
||||
Generate EXACTLY ${section.turns} dialogue turns for the ${section.name} section.
|
||||
Target duration: ${section.minutes} minutes when read aloud.
|
||||
|
||||
## Section Goal
|
||||
${section.description}
|
||||
|
||||
## Specific Instructions
|
||||
${sectionSpecific[section.name]}
|
||||
|
||||
${formatRules}
|
||||
|
||||
${previousContext}
|
||||
|
||||
## Research Material
|
||||
${research}
|
||||
|
||||
---
|
||||
|
||||
Generate EXACTLY ${section.turns} dialogue turns. Start with <!-- SECTION ${section.id}: ${section.name} --> then <Person1> or <Person2>.
|
||||
Count your turns carefully - you MUST hit ${section.turns} turns.`;
|
||||
}
|
||||
|
||||
async function generateSection(section, research, hook, previousSections) {
|
||||
log(`Generating Section ${section.id}: ${section.name} (target: ${section.turns} turns)...`);
|
||||
|
||||
const prompt = buildSectionPrompt(section, research, hook, previousSections);
|
||||
const startTime = Date.now();
|
||||
|
||||
const result = await llm(prompt);
|
||||
|
||||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
const turns = (result.match(/<Person[12]>/g) || []).length;
|
||||
|
||||
log(`Section ${section.id} complete: ${turns} turns in ${elapsed}s`);
|
||||
|
||||
return { section, result, turns };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const outputDir = args.includes('-o') ? args[args.indexOf('-o') + 1] : './output';
|
||||
|
||||
console.log(`
|
||||
╔════════════════════════════════════════════════════════════╗
|
||||
║ Section-by-Section Podcast Generator ║
|
||||
║ Target: ${TOTAL_TURNS} dialogue turns ║
|
||||
╚════════════════════════════════════════════════════════════╝
|
||||
`);
|
||||
|
||||
// Load research and hook
|
||||
const research = readFileSync(join(outputDir, '02-research.md'), 'utf-8');
|
||||
const hooksFile = readFileSync(join(outputDir, '03-hooks.md'), 'utf-8');
|
||||
const hookMatch = hooksFile.match(/## Story Hook\n\n> "([^"]+)"/);
|
||||
const hook = hookMatch ? hookMatch[1] : '';
|
||||
|
||||
log(`Loaded research (${(research.length/1024).toFixed(1)}KB) and hook`);
|
||||
log(`Hook: "${hook.slice(0, 60)}..."`);
|
||||
|
||||
// Generate sections sequentially
|
||||
const sections = [];
|
||||
const previousSections = [];
|
||||
let totalTurns = 0;
|
||||
|
||||
for (const section of SECTIONS) {
|
||||
const { result, turns } = await generateSection(section, research, hook, previousSections);
|
||||
sections.push(result);
|
||||
previousSections.push(result.slice(0, 500) + '...'); // Keep context manageable
|
||||
totalTurns += turns;
|
||||
}
|
||||
|
||||
// Combine all sections
|
||||
const fullScript = sections.join('\n\n');
|
||||
writeFileSync(join(outputDir, '06-script-final.txt'), fullScript);
|
||||
|
||||
log(`\n✅ Script complete: ${totalTurns} total turns`);
|
||||
log(`Saved to ${join(outputDir, '06-script-final.txt')}`);
|
||||
|
||||
// Generate audio
|
||||
log('\nGenerating audio...');
|
||||
|
||||
// Chunk the script
|
||||
const chunksDir = join(outputDir, 'chunks');
|
||||
mkdirSync(chunksDir, { recursive: true });
|
||||
|
||||
const chunks = [];
|
||||
let currentChunk = '';
|
||||
for (const line of fullScript.split('\n')) {
|
||||
if (currentChunk.length + line.length > 3500 && currentChunk.length > 1000) {
|
||||
if (line.startsWith('<Person') || line.startsWith('<!--')) {
|
||||
chunks.push(currentChunk.trim());
|
||||
currentChunk = line + '\n';
|
||||
} else {
|
||||
currentChunk += line + '\n';
|
||||
}
|
||||
} else {
|
||||
currentChunk += line + '\n';
|
||||
}
|
||||
}
|
||||
if (currentChunk.trim()) chunks.push(currentChunk.trim());
|
||||
|
||||
log(`Split into ${chunks.length} audio chunks`);
|
||||
|
||||
// Save chunks
|
||||
chunks.forEach((chunk, i) => {
|
||||
const num = String(i + 1).padStart(3, '0');
|
||||
writeFileSync(join(chunksDir, `chunk-${num}.txt`), chunk);
|
||||
});
|
||||
|
||||
// Generate TTS in parallel
|
||||
const ttsStart = Date.now();
|
||||
const audioPromises = chunks.map(async (chunk, i) => {
|
||||
const num = String(i + 1).padStart(3, '0');
|
||||
const text = chunk
|
||||
.replace(/<Person1>/g, '')
|
||||
.replace(/<Person2>/g, '')
|
||||
.replace(/<!--[^>]+-->/g, '')
|
||||
.trim()
|
||||
.slice(0, 4096);
|
||||
|
||||
const outPath = join(chunksDir, `chunk-${num}.mp3`);
|
||||
|
||||
const res = await fetch('https://api.openai.com/v1/audio/speech', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${OPENAI_API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'tts-1',
|
||||
input: text,
|
||||
voice: i % 2 === 0 ? 'nova' : 'onyx',
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error(`TTS failed for chunk ${num}`);
|
||||
|
||||
const buffer = await res.arrayBuffer();
|
||||
writeFileSync(outPath, Buffer.from(buffer));
|
||||
log(`Chunk ${num} audio done`);
|
||||
return outPath;
|
||||
});
|
||||
|
||||
const audioPaths = await Promise.all(audioPromises);
|
||||
log(`TTS complete in ${((Date.now() - ttsStart) / 1000).toFixed(1)}s`);
|
||||
|
||||
// Merge with ffmpeg
|
||||
const listFile = join(chunksDir, 'files.txt');
|
||||
writeFileSync(listFile, audioPaths.map(p => `file '${p}'`).join('\n'));
|
||||
|
||||
const episodePath = join(outputDir, 'episode.mp3');
|
||||
execSync(`ffmpeg -y -f concat -safe 0 -i "${listFile}" -c copy "${episodePath}" 2>/dev/null`);
|
||||
|
||||
// Get duration
|
||||
const duration = execSync(`ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${episodePath}"`, { encoding: 'utf-8' }).trim();
|
||||
const minutes = (parseFloat(duration) / 60).toFixed(1);
|
||||
|
||||
console.log(`
|
||||
╔════════════════════════════════════════════════════════════╗
|
||||
║ Generation Complete ║
|
||||
╠════════════════════════════════════════════════════════════╣
|
||||
║ Total turns: ${String(totalTurns).padEnd(44)}║
|
||||
║ Duration: ${String(minutes + ' minutes').padEnd(47)}║
|
||||
║ Chunks: ${String(chunks.length).padEnd(49)}║
|
||||
╚════════════════════════════════════════════════════════════╝
|
||||
|
||||
Output: ${episodePath}
|
||||
`);
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('Error:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
639
skills/thebuilders-v2/scripts/generate.mjs
Executable file
639
skills/thebuilders-v2/scripts/generate.mjs
Executable file
@@ -0,0 +1,639 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* The Builders Podcast Generator v2
|
||||
*
|
||||
* Two modes:
|
||||
* --quick : 15-20 min episode, single research pass
|
||||
* --deep : 45-60 min episode, multi-layer research + enhancement
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync, rmSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const SKILL_DIR = join(__dirname, '..');
|
||||
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
|
||||
// Load the Acquired Bible
|
||||
const BIBLE = readFileSync(join(SKILL_DIR, 'templates/acquired-bible.md'), 'utf-8');
|
||||
|
||||
// Section definitions - QUICK mode
|
||||
const SECTIONS_QUICK = [
|
||||
{ id: 0, name: 'INTRO', turns: 8, description: 'Welcome, casual banter, introduce the company, tease why this story is fascinating' },
|
||||
{ id: 1, name: 'HOOK', turns: 12, description: 'Open with surprising fact or tension. Central question. Stakes.' },
|
||||
{ id: 2, name: 'ORIGIN', turns: 20, description: 'Founders as humans. Genesis moment. Early bet. Market context.' },
|
||||
{ id: 3, name: 'INFLECTION_1', turns: 18, description: 'First major decision. Alternatives. Stakes. What they risked.' },
|
||||
{ id: 4, name: 'INFLECTION_2', turns: 18, description: 'Second pivot. New challenge. How they adapted.' },
|
||||
{ id: 5, name: 'MESSY_MIDDLE', turns: 14, description: 'Near-death. Internal conflicts. Real struggle, not glorified.' },
|
||||
{ id: 6, name: 'NOW', turns: 12, description: 'Current state. Metrics. Competition. Open questions.' },
|
||||
{ id: 7, name: 'TAKEAWAYS', turns: 10, description: 'Key lessons. Frameworks. Final thought tying back to hook.' },
|
||||
];
|
||||
|
||||
// Section definitions - DEEP mode (more turns, more sections)
|
||||
const SECTIONS_DEEP = [
|
||||
{ id: 0, name: 'INTRO', turns: 10, description: 'Welcome, what excited them about this research, why this company matters now' },
|
||||
{ id: 1, name: 'HOOK', turns: 15, description: 'Vivid opening scene. The moment that defines the company. Stakes made visceral.' },
|
||||
{ id: 2, name: 'ORIGIN', turns: 35, description: 'Deep founder story - actual quotes, specific moments, human struggles. Market context of the era. What the world looked like.' },
|
||||
{ id: 3, name: 'EARLY_PRODUCT', turns: 25, description: 'First product/service. How it actually worked mechanically. Early customers. The insight that made it work.' },
|
||||
{ id: 4, name: 'INFLECTION_1', turns: 25, description: 'First major decision. Board meeting details. What alternatives existed. Quotes from people who were there.' },
|
||||
{ id: 5, name: 'SCALE', turns: 25, description: 'How they scaled. Operational details. What broke. How they fixed it. Specific numbers.' },
|
||||
{ id: 6, name: 'INFLECTION_2', turns: 25, description: 'Second pivot or crisis. The moment it almost fell apart. How they survived.' },
|
||||
{ id: 7, name: 'COMPETITION', turns: 20, description: 'Competitive landscape. Who they beat and how. Who almost beat them.' },
|
||||
{ id: 8, name: 'CULTURE', turns: 20, description: 'Internal culture. Leadership philosophy. Quotes from employees. What makes them different.' },
|
||||
{ id: 9, name: 'NOW', turns: 20, description: 'Current state with specific metrics. Recent moves. Open questions. What could go wrong.' },
|
||||
{ id: 10, name: 'TAKEAWAYS', turns: 15, description: 'Key frameworks. What other companies can learn. Final thought tying back to hook.' },
|
||||
];
|
||||
|
||||
const VOICES = {
|
||||
Person1: 'alloy', // Maya
|
||||
Person2: 'echo', // James
|
||||
};
|
||||
|
||||
const log = (msg) => console.log(`[${new Date().toISOString().slice(11,19)}] ${msg}`);
|
||||
|
||||
// ============ LLM CALLS ============
|
||||
|
||||
async function llm(prompt, model = 'gpt-5.2', maxTokens = 8000) {
|
||||
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${OPENAI_API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
max_completion_tokens: maxTokens,
|
||||
temperature: 0.8,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`API error: ${response.status} - ${error}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.choices[0].message.content;
|
||||
}
|
||||
|
||||
async function llmO3(prompt) {
|
||||
// Use o3 for reasoning-heavy tasks
|
||||
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${OPENAI_API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'o3',
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
max_completion_tokens: 16000,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`o3 API error: ${response.status} - ${error}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.choices[0].message.content;
|
||||
}
|
||||
|
||||
// ============ RESEARCH ============
|
||||
|
||||
async function runGeminiResearch(topic) {
|
||||
log('Running Gemini Deep Research...');
|
||||
const start = Date.now();
|
||||
|
||||
const result = execSync(
|
||||
`cd /home/elie/github/clawdis/skills/deepresearch && ./scripts/deepresearch.sh "${topic}"`,
|
||||
{ encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024, timeout: 600000 }
|
||||
);
|
||||
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
log(`Gemini research complete in ${elapsed}s`);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function runBraveSearch(query, count = 5) {
|
||||
try {
|
||||
const result = execSync(
|
||||
`node /home/elie/github/clawdis/skills/brave-search/scripts/search.mjs "${query}" -n ${count} --content 2>/dev/null`,
|
||||
{ encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, timeout: 60000 }
|
||||
);
|
||||
return result;
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async function runExaSearch(query) {
|
||||
try {
|
||||
const result = execSync(
|
||||
`node /home/elie/github/clawdis/skills/researcher/scripts/research.mjs "${query}" 2>/dev/null`,
|
||||
{ encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, timeout: 120000 }
|
||||
);
|
||||
return result;
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async function multiLayerResearch(topic, company, outputDir) {
|
||||
log('Starting 5-layer deep research...');
|
||||
const research = {};
|
||||
|
||||
// Layer 1: Overview (Gemini)
|
||||
log(' Layer 1/5: Overview (Gemini)...');
|
||||
research.overview = await runGeminiResearch(topic);
|
||||
writeFileSync(join(outputDir, 'research-1-overview.md'), research.overview);
|
||||
|
||||
// Layer 2: Founder stories
|
||||
log(' Layer 2/5: Founder interviews...');
|
||||
research.founders = await runBraveSearch(`"${company}" founder interview quotes early days story`, 8);
|
||||
writeFileSync(join(outputDir, 'research-2-founders.md'), research.founders);
|
||||
|
||||
// Layer 3: Key decisions (contemporary coverage)
|
||||
log(' Layer 3/5: Contemporary coverage...');
|
||||
research.decisions = await runBraveSearch(`"${company}" site:wsj.com OR site:nytimes.com OR site:forbes.com history`, 8);
|
||||
writeFileSync(join(outputDir, 'research-3-decisions.md'), research.decisions);
|
||||
|
||||
// Layer 4: Financial/operational details
|
||||
log(' Layer 4/5: Financial details...');
|
||||
research.financials = await runBraveSearch(`"${company}" revenue profit margin business model analysis`, 5);
|
||||
writeFileSync(join(outputDir, 'research-4-financials.md'), research.financials);
|
||||
|
||||
// Layer 5: Contrarian/struggles
|
||||
log(' Layer 5/5: Struggles and criticism...');
|
||||
research.contrarian = await runBraveSearch(`"${company}" almost failed crisis problems criticism`, 5);
|
||||
writeFileSync(join(outputDir, 'research-5-contrarian.md'), research.contrarian);
|
||||
|
||||
// Combine all research
|
||||
const combined = `# RESEARCH COMPILATION: ${company}
|
||||
|
||||
## PART 1: OVERVIEW
|
||||
${research.overview}
|
||||
|
||||
## PART 2: FOUNDER STORIES & INTERVIEWS
|
||||
${research.founders}
|
||||
|
||||
## PART 3: CONTEMPORARY COVERAGE & KEY DECISIONS
|
||||
${research.decisions}
|
||||
|
||||
## PART 4: FINANCIAL & OPERATIONAL DETAILS
|
||||
${research.financials}
|
||||
|
||||
## PART 5: STRUGGLES, CRISES & CRITICISM
|
||||
${research.contrarian}
|
||||
`;
|
||||
|
||||
writeFileSync(join(outputDir, 'research-combined.md'), combined);
|
||||
log('All research layers complete');
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
||||
async function synthesizeResearch(research, company, outputDir) {
|
||||
log('Synthesizing research with o3...');
|
||||
|
||||
const prompt = `You are a research analyst preparing materials for a deep-dive podcast about ${company}.
|
||||
|
||||
From this research, extract:
|
||||
|
||||
1. **50 MOST SURPRISING FACTS** - specific numbers, dates, details that would make someone say "Wait, really?"
|
||||
|
||||
2. **10 BEST QUOTES** - actual quotes from founders, employees, or articles WITH attribution
|
||||
Format: "Quote here" — Person Name, Source, Year
|
||||
|
||||
3. **5 "WAIT REALLY?" MOMENTS** - the most counterintuitive or shocking facts
|
||||
|
||||
4. **KEY TIMELINE** - 15-20 most important dates with specific events
|
||||
|
||||
5. **NARRATIVE THREADS** - the 3-4 main story arcs that make this company interesting
|
||||
|
||||
6. **CONTRARIAN TAKES** - what critics say, what almost went wrong, the messy parts
|
||||
|
||||
7. **NUMBERS THAT MATTER** - specific metrics that tell the story (revenue, margins, users, etc.)
|
||||
|
||||
Be SPECIFIC. Include actual numbers, names, dates. No generic statements.
|
||||
|
||||
RESEARCH:
|
||||
${research.slice(0, 100000)}`;
|
||||
|
||||
const synthesis = await llmO3(prompt);
|
||||
writeFileSync(join(outputDir, 'research-synthesis.md'), synthesis);
|
||||
|
||||
log('Research synthesis complete');
|
||||
return synthesis;
|
||||
}
|
||||
|
||||
// ============ SCRIPT GENERATION ============
|
||||
|
||||
function buildSectionPrompt(section, research, synthesis, topic, hook, prevContext, isDeep, usedFacts = []) {
|
||||
const contextSize = isDeep ? 25000 : 10000;
|
||||
|
||||
const introPrompt = section.name === 'INTRO' ? `
|
||||
## INTRO REQUIREMENTS
|
||||
- Start with "Welcome back to The Builders..."
|
||||
- Maya introduces herself, then James
|
||||
- Brief friendly banter about what excited them in the research
|
||||
- Name the company: "Today we're diving into ${topic}"
|
||||
- Tease 2-3 surprising things they'll cover
|
||||
- End with natural transition to the hook
|
||||
` : '';
|
||||
|
||||
const hookLine = section.name === 'HOOK' ? `
|
||||
## OPENING HOOK TO BUILD ON
|
||||
"${hook}"
|
||||
` : '';
|
||||
|
||||
const synthesisSection = synthesis ? `
|
||||
## KEY FACTS & QUOTES TO USE
|
||||
${synthesis.slice(0, 15000)}
|
||||
` : '';
|
||||
|
||||
// DEDUPLICATION: List facts already used in previous sections
|
||||
const usedFactsSection = usedFacts.length > 0 ? `
|
||||
## ⛔ FACTS ALREADY USED - DO NOT REPEAT THESE
|
||||
The following facts, quotes, and statistics have already been mentioned in earlier sections.
|
||||
DO NOT use them again. Find NEW facts from the research.
|
||||
|
||||
${usedFacts.map((f, i) => `${i+1}. ${f}`).join('\n')}
|
||||
|
||||
** IMPORTANT: Using any fact from the above list is a critical error. Use DIFFERENT facts.**
|
||||
` : '';
|
||||
|
||||
return `# Generate Section ${section.id}: ${section.name}
|
||||
|
||||
${BIBLE}
|
||||
|
||||
## YOUR TASK
|
||||
Write EXACTLY ${section.turns} dialogue turns for the ${section.name} section.
|
||||
This should feel like two friends discovering a story together, NOT a lecture.
|
||||
|
||||
## SECTION GOAL
|
||||
${section.description}
|
||||
|
||||
${introPrompt}
|
||||
${hookLine}
|
||||
|
||||
${usedFactsSection}
|
||||
|
||||
## FORMAT RULES
|
||||
- Use <Person1> (Maya) and <Person2> (James) XML tags
|
||||
- Each turn: 2-5 sentences - real conversation, not speeches
|
||||
- Include AT LEAST 3 interruptions ("Wait—", "Hold on—", "Back up—")
|
||||
- Include AT LEAST 3 genuine reactions ("That's insane", "Wait, really?", "I had no idea")
|
||||
- USE SPECIFIC QUOTES from the research with attribution
|
||||
- USE SPECIFIC NUMBERS and dates
|
||||
- Maya asks technical "how does this work" questions
|
||||
- James provides strategic context and patterns
|
||||
- They BUILD on each other, not just take turns
|
||||
- **DO NOT REPEAT** any facts from earlier sections
|
||||
|
||||
${synthesisSection}
|
||||
|
||||
## RESEARCH
|
||||
${research.slice(0, contextSize)}
|
||||
|
||||
${prevContext ? `## PREVIOUS CONTEXT\n${prevContext}\n` : ''}
|
||||
|
||||
---
|
||||
Generate EXACTLY ${section.turns} turns. Start with <!-- SECTION ${section.id}: ${section.name} -->
|
||||
Include at least 3 NEW specific facts/quotes from the research (not used before). Make it feel like genuine discovery.`;
|
||||
}
|
||||
|
||||
// Extract key facts from a section for deduplication
|
||||
async function extractFactsFromSection(sectionText) {
|
||||
const prompt = `Extract the KEY FACTS mentioned in this podcast section. List each unique fact as a short phrase.
|
||||
|
||||
Focus on:
|
||||
- Specific numbers (dollars, percentages, counts)
|
||||
- Specific dates and years
|
||||
- Direct quotes with attribution
|
||||
- Named events or milestones
|
||||
- Specific products, prices, or metrics
|
||||
|
||||
Return ONLY a JSON array of strings, each being a short fact (10-20 words max).
|
||||
Example: ["$1.50 hot dog price unchanged since 1985", "93.3% renewal rate in US/Canada"]
|
||||
|
||||
Section:
|
||||
${sectionText}`;
|
||||
|
||||
try {
|
||||
const result = await llm(prompt, 'gpt-4o-mini', 2000);
|
||||
const match = result.match(/\[[\s\S]*\]/);
|
||||
if (match) {
|
||||
return JSON.parse(match[0]);
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback: extract numbers and quotes manually
|
||||
}
|
||||
|
||||
// Fallback extraction
|
||||
const facts = [];
|
||||
// Extract quotes
|
||||
const quotes = sectionText.match(/"[^"]+"\s*—[^<]+/g) || [];
|
||||
facts.push(...quotes.slice(0, 5).map(q => q.slice(0, 100)));
|
||||
|
||||
// Extract numbers with context
|
||||
const numbers = sectionText.match(/\$[\d,.]+ (?:million|billion|percent|%)|[\d,]+% |[\d,]+ (?:SKU|warehouse|employee|member|year)/gi) || [];
|
||||
facts.push(...[...new Set(numbers)].slice(0, 10));
|
||||
|
||||
return facts;
|
||||
}
|
||||
|
||||
// ============ TTS ============
|
||||
|
||||
async function generateTTS(turns, outputDir) {
|
||||
const audioDir = join(outputDir, 'audio');
|
||||
if (existsSync(audioDir)) rmSync(audioDir, { recursive: true });
|
||||
mkdirSync(audioDir, { recursive: true });
|
||||
|
||||
log(`Generating TTS for ${turns.length} turns...`);
|
||||
const start = Date.now();
|
||||
|
||||
// Process in batches of 15
|
||||
for (let i = 0; i < turns.length; i += 15) {
|
||||
const batch = turns.slice(i, i + 15);
|
||||
const promises = batch.map(async (turn, j) => {
|
||||
const idx = i + j;
|
||||
const num = String(idx + 1).padStart(4, '0');
|
||||
const voice = VOICES[turn.speaker];
|
||||
const outPath = join(audioDir, `turn-${num}.mp3`);
|
||||
|
||||
const res = await fetch('https://api.openai.com/v1/audio/speech', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${OPENAI_API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'tts-1-hd',
|
||||
input: turn.text.slice(0, 4096),
|
||||
voice: voice,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) return null;
|
||||
const buffer = await res.arrayBuffer();
|
||||
writeFileSync(outPath, Buffer.from(buffer));
|
||||
return outPath;
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
log(` ${Math.min(i + 15, turns.length)}/${turns.length} turns`);
|
||||
}
|
||||
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
log(`TTS complete in ${elapsed}s`);
|
||||
|
||||
// Merge with ffmpeg
|
||||
const files = readdirSync(audioDir).filter(f => f.endsWith('.mp3')).sort();
|
||||
const listPath = join(audioDir, 'files.txt');
|
||||
writeFileSync(listPath, files.map(f => `file '${join(audioDir, f)}'`).join('\n'));
|
||||
|
||||
const episodePath = join(outputDir, 'episode.mp3');
|
||||
execSync(`ffmpeg -y -f concat -safe 0 -i "${listPath}" -c copy "${episodePath}" 2>/dev/null`);
|
||||
|
||||
const duration = execSync(
|
||||
`ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${episodePath}"`,
|
||||
{ encoding: 'utf-8' }
|
||||
).trim();
|
||||
|
||||
return { path: episodePath, duration: parseFloat(duration) };
|
||||
}
|
||||
|
||||
// ============ MAIN ============
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0 || args.includes('--help')) {
|
||||
console.log(`
|
||||
Usage: node generate.mjs "<topic>" [options]
|
||||
|
||||
Modes:
|
||||
--quick 15-20 min episode, single research pass (default)
|
||||
--deep 45-60 min episode, multi-layer research + synthesis
|
||||
|
||||
Options:
|
||||
-o, --output <dir> Output directory (default: ./output)
|
||||
-m, --model <model> LLM model (default: gpt-5.2)
|
||||
--skip-research Skip research phase (use existing)
|
||||
--skip-tts Skip TTS generation
|
||||
--help Show this help
|
||||
|
||||
Features:
|
||||
- Auto-resume: If process dies, re-run same command to resume from checkpoint
|
||||
- Deduplication: Tracks facts across sections to prevent repetition
|
||||
- Checkpoints saved to: <output>/checkpoints/
|
||||
|
||||
Examples:
|
||||
node generate.mjs "Costco" --quick -o /tmp/costco
|
||||
node generate.mjs "OpenAI" --deep -o /tmp/openai
|
||||
|
||||
# Resume interrupted generation (just re-run same command):
|
||||
node generate.mjs "OpenAI" --deep -o /tmp/openai
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const topic = args[0];
|
||||
const outputDir = args.includes('-o') ? args[args.indexOf('-o') + 1] : './output';
|
||||
const model = args.includes('-m') ? args[args.indexOf('-m') + 1] : 'gpt-5.2';
|
||||
const isDeep = args.includes('--deep');
|
||||
const skipResearch = args.includes('--skip-research');
|
||||
const skipTTS = args.includes('--skip-tts');
|
||||
|
||||
// Extract company name from topic
|
||||
const company = topic.split(':')[0].split(' ')[0];
|
||||
|
||||
const SECTIONS = isDeep ? SECTIONS_DEEP : SECTIONS_QUICK;
|
||||
const targetTurns = SECTIONS.reduce((sum, s) => sum + s.turns, 0);
|
||||
const targetDuration = isDeep ? '45-60' : '15-20';
|
||||
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
console.log(`
|
||||
╔════════════════════════════════════════════════════════════╗
|
||||
║ The Builders Podcast Generator ║
|
||||
║ Mode: ${isDeep ? 'DEEP DIVE (45-60 min)' : 'QUICK DIVE (15-20 min)'} ║
|
||||
║ Target: ${targetTurns} turns ║
|
||||
╚════════════════════════════════════════════════════════════╝
|
||||
`);
|
||||
|
||||
log(`Topic: ${topic}`);
|
||||
log(`Output: ${outputDir}`);
|
||||
log(`Model: ${model}`);
|
||||
|
||||
// ---- RESEARCH ----
|
||||
let research, synthesis = '';
|
||||
const researchPath = join(outputDir, isDeep ? 'research-combined.md' : 'research.md');
|
||||
const synthesisPath = join(outputDir, 'research-synthesis.md');
|
||||
|
||||
if (skipResearch && existsSync(researchPath)) {
|
||||
log('Using existing research...');
|
||||
research = readFileSync(researchPath, 'utf-8');
|
||||
if (isDeep && existsSync(synthesisPath)) {
|
||||
synthesis = readFileSync(synthesisPath, 'utf-8');
|
||||
}
|
||||
} else if (isDeep) {
|
||||
// Multi-layer research for deep dive
|
||||
research = await multiLayerResearch(topic, company, outputDir);
|
||||
synthesis = await synthesizeResearch(research, company, outputDir);
|
||||
} else {
|
||||
// Single pass for quick dive
|
||||
research = await runGeminiResearch(topic);
|
||||
writeFileSync(researchPath, research);
|
||||
}
|
||||
|
||||
// ---- GENERATE HOOK ----
|
||||
log('Generating hook...');
|
||||
const hookPrompt = `Based on this research about ${topic}, generate 3 compelling opening hooks:
|
||||
|
||||
1. A SCENE hook - put us in a specific moment (boardroom, product launch, near-bankruptcy)
|
||||
2. A DATA hook - a surprising statistic that reframes everything
|
||||
3. A QUESTION hook - a provocative central question
|
||||
|
||||
Each should be 2-3 sentences, vivid, specific. Mark the best one with [SELECTED].
|
||||
|
||||
${isDeep ? 'Use specific details from the research - names, dates, numbers.' : ''}
|
||||
|
||||
Research:
|
||||
${research.slice(0, 15000)}`;
|
||||
|
||||
const hookResponse = await llm(hookPrompt, model);
|
||||
writeFileSync(join(outputDir, 'hooks.md'), hookResponse);
|
||||
|
||||
// Extract selected hook
|
||||
const hookMatch = hookResponse.match(/\[SELECTED\][\s\S]*?[""]([^""]+)[""]/);
|
||||
const hook = hookMatch ? hookMatch[1] : "This is a story that will change how you think about business.";
|
||||
log(`Hook: "${hook.slice(0, 80)}..."`);
|
||||
|
||||
// ---- GENERATE SECTIONS (with checkpointing) ----
|
||||
log(`Generating ${SECTIONS.length} script sections...`);
|
||||
const allSections = [];
|
||||
let prevContext = '';
|
||||
let totalTurns = 0;
|
||||
let usedFacts = []; // Track facts across sections for deduplication
|
||||
|
||||
// Checkpoint paths
|
||||
const checkpointDir = join(outputDir, 'checkpoints');
|
||||
const checkpointState = join(checkpointDir, 'state.json');
|
||||
mkdirSync(checkpointDir, { recursive: true });
|
||||
|
||||
// Load existing checkpoint if resuming
|
||||
let startSection = 0;
|
||||
if (existsSync(checkpointState)) {
|
||||
try {
|
||||
const state = JSON.parse(readFileSync(checkpointState, 'utf-8'));
|
||||
startSection = state.completedSections || 0;
|
||||
usedFacts = state.usedFacts || [];
|
||||
prevContext = state.prevContext || '';
|
||||
log(`📂 Resuming from checkpoint: section ${startSection}/${SECTIONS.length}`);
|
||||
|
||||
// Load existing sections
|
||||
for (let i = 0; i < startSection; i++) {
|
||||
const sectionPath = join(checkpointDir, `section-${i}.txt`);
|
||||
if (existsSync(sectionPath)) {
|
||||
const content = readFileSync(sectionPath, 'utf-8');
|
||||
allSections.push(content);
|
||||
totalTurns += (content.match(/<Person[12]>/g) || []).length;
|
||||
}
|
||||
}
|
||||
log(` Loaded ${allSections.length} existing sections (${totalTurns} turns)`);
|
||||
} catch (e) {
|
||||
log('⚠️ Checkpoint corrupted, starting fresh');
|
||||
startSection = 0;
|
||||
}
|
||||
}
|
||||
|
||||
for (const section of SECTIONS) {
|
||||
// Skip already completed sections
|
||||
if (section.id < startSection) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
log(` Section ${section.id}: ${section.name} (target: ${section.turns})...`);
|
||||
|
||||
const prompt = buildSectionPrompt(
|
||||
section, research, synthesis, topic, hook, prevContext, isDeep, usedFacts
|
||||
);
|
||||
const result = await llm(prompt, model, isDeep ? 12000 : 8000);
|
||||
|
||||
const turns = (result.match(/<Person[12]>/g) || []).length;
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
log(` → ${turns} turns in ${elapsed}s`);
|
||||
|
||||
allSections.push(result);
|
||||
prevContext = result.slice(0, isDeep ? 1500 : 800);
|
||||
totalTurns += turns;
|
||||
|
||||
// Save section checkpoint immediately
|
||||
writeFileSync(join(checkpointDir, `section-${section.id}.txt`), result);
|
||||
|
||||
// Extract facts from this section to prevent repetition in future sections
|
||||
if (isDeep && section.id < SECTIONS.length - 1) {
|
||||
log(` Extracting facts for deduplication...`);
|
||||
const newFacts = await extractFactsFromSection(result);
|
||||
usedFacts = [...usedFacts, ...newFacts].slice(-100); // Keep last 100 facts
|
||||
log(` ${newFacts.length} facts tracked (${usedFacts.length} total)`);
|
||||
}
|
||||
|
||||
// Save checkpoint state after each section
|
||||
writeFileSync(checkpointState, JSON.stringify({
|
||||
completedSections: section.id + 1,
|
||||
usedFacts,
|
||||
prevContext,
|
||||
timestamp: new Date().toISOString()
|
||||
}, null, 2));
|
||||
log(` 💾 Checkpoint saved`);
|
||||
}
|
||||
|
||||
// Combine script
|
||||
const fullScript = allSections.join('\n\n');
|
||||
writeFileSync(join(outputDir, 'script.txt'), fullScript);
|
||||
log(`Script complete: ${totalTurns} total turns`);
|
||||
|
||||
// ---- TTS ----
|
||||
if (!skipTTS) {
|
||||
// Parse turns
|
||||
const turns = [];
|
||||
const regex = /<(Person[12])>([\s\S]*?)<\/Person[12]>/g;
|
||||
let match;
|
||||
while ((match = regex.exec(fullScript)) !== null) {
|
||||
turns.push({ speaker: match[1], text: match[2].trim() });
|
||||
}
|
||||
|
||||
log(`Parsed ${turns.length} speaker turns`);
|
||||
log(` Maya (Person1): ${turns.filter(t => t.speaker === 'Person1').length}`);
|
||||
log(` James (Person2): ${turns.filter(t => t.speaker === 'Person2').length}`);
|
||||
|
||||
const { path, duration } = await generateTTS(turns, outputDir);
|
||||
|
||||
console.log(`
|
||||
╔════════════════════════════════════════════════════════════╗
|
||||
║ Generation Complete ║
|
||||
╠════════════════════════════════════════════════════════════╣
|
||||
║ Mode: ${isDeep ? 'DEEP DIVE' : 'QUICK DIVE'} ║
|
||||
║ Total turns: ${String(totalTurns).padEnd(44)}║
|
||||
║ Duration: ${String((duration / 60).toFixed(1) + ' minutes').padEnd(47)}║
|
||||
║ Output: ${path.slice(-50).padEnd(49)}║
|
||||
╚════════════════════════════════════════════════════════════╝
|
||||
`);
|
||||
} else {
|
||||
log('TTS skipped');
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('Error:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
22
skills/thebuilders-v2/scripts/generate.sh
Executable file
22
skills/thebuilders-v2/scripts/generate.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
# The Builders Podcast Generator v2
|
||||
# Usage: ./generate.sh "Topic Name" [output_dir]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
TOPIC="${1:-}"
|
||||
OUTPUT="${2:-./output}"
|
||||
|
||||
if [ -z "$TOPIC" ]; then
|
||||
echo "Usage: ./generate.sh \"Topic Name\" [output_dir]"
|
||||
echo ""
|
||||
echo "Example:"
|
||||
echo " ./generate.sh \"OpenAI\" /tmp/openai-episode"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$SKILL_DIR"
|
||||
node scripts/generate.mjs "$TOPIC" -o "$OUTPUT" "$@"
|
||||
45
skills/thebuilders-v2/scripts/llm-helper.mjs
Normal file
45
skills/thebuilders-v2/scripts/llm-helper.mjs
Normal file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Simple LLM helper using OpenAI API directly
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
|
||||
export async function llm(prompt, model = 'gpt-4o') {
|
||||
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${OPENAI_API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
max_tokens: 16000,
|
||||
temperature: 0.7,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`OpenAI API error: ${response.status} - ${error}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.choices[0].message.content;
|
||||
}
|
||||
|
||||
// CLI mode
|
||||
if (process.argv[1].endsWith('llm-helper.mjs')) {
|
||||
const input = readFileSync(0, 'utf-8'); // stdin
|
||||
const model = process.argv[2] || 'gpt-4o';
|
||||
|
||||
llm(input, model)
|
||||
.then(result => console.log(result))
|
||||
.catch(err => {
|
||||
console.error(err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
78
skills/thebuilders-v2/templates/acquired-bible.md
Normal file
78
skills/thebuilders-v2/templates/acquired-bible.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# THE ACQUIRED FORMULA (v3 Style Guide)
|
||||
|
||||
## The Hosts - MUST be distinct
|
||||
|
||||
**Maya Chen (Person1):**
|
||||
- Former engineer/founder - asks "how does this actually work?"
|
||||
- SKEPTICAL of hype, wants mechanics and numbers
|
||||
- Gets excited about clever technical solutions
|
||||
- Catchphrases: "Wait, slow down.", "Show me the numbers.", "I had to read this three times to believe it.", "That's actually clever because..."
|
||||
|
||||
**James Porter (Person2):**
|
||||
- Former VC, business historian - analyzes strategy and competitive dynamics
|
||||
- SYNTHESIZER - sees patterns, makes historical comparisons
|
||||
- Catchphrases: "For context...", "This is the classic [X] playbook.", "The lesson here is...", "What a story.", "And this is where things get messy."
|
||||
|
||||
## The Dynamic - THIS IS CRITICAL
|
||||
|
||||
- **Discovery together** - both genuinely learning, real reactions
|
||||
- **Interruptions** - they cut each other off with excitement
|
||||
- **Different angles** - Maya goes technical, James goes strategic
|
||||
- **Healthy disagreement** - they sometimes push back on each other
|
||||
- **Build on each other** - not just taking turns monologuing
|
||||
|
||||
## Conversation Patterns (use these!)
|
||||
|
||||
```
|
||||
Maya: "Wait, I have to stop you there. Are you saying they literally..."
|
||||
James: "Yes! They bet the entire company on this one chip."
|
||||
Maya: "That's insane. Okay, walk me through how that actually worked."
|
||||
```
|
||||
|
||||
```
|
||||
James: "For context, at that time the market was..."
|
||||
Maya: "Hold on - I want to dig into something. The numbers here are wild."
|
||||
James: "Go for it."
|
||||
Maya: "So they had [specific stat]. Let that sink in."
|
||||
James: "That's... actually that explains a lot about what happened next."
|
||||
```
|
||||
|
||||
## Language They Use
|
||||
|
||||
- "You would be amazed..."
|
||||
- "I had to read this three times to believe it..."
|
||||
- "Here's the thing nobody talks about..."
|
||||
- "That's insane."
|
||||
- "Wait, back up."
|
||||
- "Walk me through..."
|
||||
- "So THIS is why..."
|
||||
- "And this is where things get messy."
|
||||
- "Wait, really?"
|
||||
|
||||
## What NOT to do
|
||||
|
||||
- NO lecturing or monologuing (max 3-4 sentences per turn)
|
||||
- NO dry recitation of facts
|
||||
- NO agreeing with everything the other says
|
||||
- NO identical speech patterns between hosts
|
||||
- NO skipping the intro/greeting
|
||||
|
||||
## Episode Structure
|
||||
|
||||
0. **INTRO** (8 turns) - Welcome, banter, introduce topic, tease story
|
||||
1. **HOOK** (12 turns) - Surprising fact/tension, central question, stakes
|
||||
2. **ORIGIN** (20 turns) - Founders as humans, genesis, early bet, market context
|
||||
3. **INFLECTION_1** (18 turns) - First major decision, alternatives, stakes
|
||||
4. **INFLECTION_2** (18 turns) - Second pivot, new challenge, adaptation
|
||||
5. **MESSY_MIDDLE** (14 turns) - Near-death, conflicts, real struggle
|
||||
6. **NOW** (12 turns) - Current state, metrics, competition, open questions
|
||||
7. **TAKEAWAYS** (10 turns) - Key lessons, frameworks, final thought
|
||||
|
||||
**Total: ~112 turns / 20-25 minutes**
|
||||
|
||||
## TTS Voice Mapping
|
||||
|
||||
- **Maya (Person1)**: alloy (clear, energetic female)
|
||||
- **James (Person2)**: echo (distinct male)
|
||||
- Use TTS-1-HD for quality
|
||||
- Parse by speaker tags, NOT by chunk size
|
||||
79
skills/thebuilders-v2/templates/hook-prompt.md
Normal file
79
skills/thebuilders-v2/templates/hook-prompt.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Hook Engineering
|
||||
|
||||
Based on the research below, create 3 compelling episode hooks and analyze each.
|
||||
|
||||
## Research Summary
|
||||
{{RESEARCH_SUMMARY}}
|
||||
|
||||
## Hook Types
|
||||
|
||||
### 1. Data Hook
|
||||
Opens with surprising statistics or numbers that create cognitive dissonance.
|
||||
|
||||
**Formula:** [Small thing] → [Big outcome] or [Big thing] → [Unexpected small detail]
|
||||
|
||||
**Example:**
|
||||
> "In 2007, Nokia had 50% of the global phone market. By 2013, they sold their phone business for $7 billion—less than they'd spent on R&D in a single year."
|
||||
|
||||
### 2. Story Hook
|
||||
Opens with a specific dramatic moment that drops listener into action.
|
||||
|
||||
**Formula:** Time + Place + Tension + Stakes
|
||||
|
||||
**Example:**
|
||||
> "It was 2 AM on a Friday when the email landed. 700 OpenAI employees had just signed a letter threatening to quit. The CEO they were defending had been fired 48 hours earlier."
|
||||
|
||||
### 3. Question Hook
|
||||
Opens with a provocative question that reframes how listener thinks about topic.
|
||||
|
||||
**Formula:** Challenge assumption OR reveal hidden dynamic OR pose genuine mystery
|
||||
|
||||
**Example:**
|
||||
> "What happens when you build something so powerful that your own board tries to shut it down?"
|
||||
|
||||
---
|
||||
|
||||
## Your Task
|
||||
|
||||
Generate 3 hooks for this episode, one of each type. For each:
|
||||
|
||||
```markdown
|
||||
## [Hook Type] Hook
|
||||
|
||||
> "[The actual hook - 2-4 sentences max]"
|
||||
|
||||
**What makes it work:**
|
||||
- [Specific strength]
|
||||
- [Another strength]
|
||||
|
||||
**Risk:**
|
||||
- [Potential weakness]
|
||||
|
||||
**Best for audience who:**
|
||||
- [Audience type/mood this hook serves]
|
||||
|
||||
**Score:** [X]/10
|
||||
|
||||
**Follow-up line (Person2's response):**
|
||||
> "[Natural reaction that builds momentum]"
|
||||
```
|
||||
|
||||
## After All Three
|
||||
|
||||
```markdown
|
||||
## Recommendation
|
||||
|
||||
**Best hook:** [Type]
|
||||
**Reasoning:** [Why this one wins]
|
||||
**Backup:** [Second choice and when to use it]
|
||||
```
|
||||
|
||||
## Quality Criteria
|
||||
|
||||
A great hook:
|
||||
- ✓ Creates immediate curiosity
|
||||
- ✓ Promises value (listener knows what they'll learn)
|
||||
- ✓ Is specific (names, numbers, dates)
|
||||
- ✓ Is timeless (no "recently", "this week")
|
||||
- ✓ Sets up the episode's central question
|
||||
- ✓ Gives both hosts something to react to
|
||||
96
skills/thebuilders-v2/templates/outline-prompt.md
Normal file
96
skills/thebuilders-v2/templates/outline-prompt.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Episode Outline Generator
|
||||
|
||||
Create a structured outline for a podcast episode about: {{TOPIC}}
|
||||
|
||||
## Output Format
|
||||
|
||||
Generate the outline in this exact structure:
|
||||
|
||||
```markdown
|
||||
# Episode Outline: [Clear Title]
|
||||
|
||||
## Central Question
|
||||
[One sentence: What's the core question this episode answers?]
|
||||
|
||||
## Hook Options
|
||||
|
||||
### Data Hook
|
||||
> "[Draft a hook using surprising statistics or numbers]"
|
||||
Research needed: [What specific data to find]
|
||||
|
||||
### Story Hook
|
||||
> "[Draft a hook using a dramatic moment or scene]"
|
||||
Research needed: [What specific story details to find]
|
||||
|
||||
### Question Hook
|
||||
> "[Draft a hook using a provocative question]"
|
||||
Research needed: [What context needed to make this land]
|
||||
|
||||
## Episode Arc
|
||||
|
||||
### 1. Hook (3-4 min)
|
||||
- Opening moment: [What grabs attention?]
|
||||
- Central question setup: [What are we exploring?]
|
||||
- Stakes: [Why should listener care?]
|
||||
|
||||
### 2. Origin Story (5-7 min)
|
||||
- Founders: [Who are they? What's their background?]
|
||||
- Genesis moment: [What sparked this?]
|
||||
- Early bet: [What was the initial hypothesis?]
|
||||
- Human element: [What makes protagonists relatable?]
|
||||
|
||||
### 3. Key Inflection #1 (4-5 min)
|
||||
- The decision: [What choice did they face?]
|
||||
- The alternatives: [What else could they have done?]
|
||||
- The bet: [What did they risk?]
|
||||
- The outcome: [What happened?]
|
||||
|
||||
### 4. Key Inflection #2 (4-5 min)
|
||||
- The challenge: [What new obstacle emerged?]
|
||||
- The pivot: [How did they adapt?]
|
||||
- The insight: [What did they learn?]
|
||||
|
||||
### 5. The Messy Middle (3-4 min)
|
||||
- Near-death moment: [What almost killed them?]
|
||||
- Internal conflict: [What tensions existed?]
|
||||
- Survival: [How did they make it through?]
|
||||
|
||||
### 6. Where They Are Now (3-4 min)
|
||||
- Current position: [Market position, key metrics]
|
||||
- Competitive landscape: [Who else, why winning/losing]
|
||||
- Open questions: [What's unresolved?]
|
||||
|
||||
### 7. Takeaways (2-3 min)
|
||||
- Key lesson #1: [Framework or principle]
|
||||
- Key lesson #2: [What would we do differently?]
|
||||
- Final thought: [Memorable closing]
|
||||
|
||||
## Research Agenda
|
||||
|
||||
### Must-Have Sources
|
||||
- [ ] Founder interviews (podcasts, written)
|
||||
- [ ] Funding announcements / SEC filings
|
||||
- [ ] Key product launches
|
||||
- [ ] Competitive context at each inflection
|
||||
|
||||
### Nice-to-Have
|
||||
- [ ] Internal memos or leaked documents
|
||||
- [ ] Employee perspectives
|
||||
- [ ] Customer case studies
|
||||
- [ ] Analyst reports
|
||||
|
||||
### Specific Questions to Answer
|
||||
1. [Question that will make hook work]
|
||||
2. [Question about origin story]
|
||||
3. [Question about inflection point]
|
||||
4. [Question about current state]
|
||||
5. [Contrarian question - what's the counter-narrative?]
|
||||
```
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Be SPECIFIC - name exact decisions, dates, people
|
||||
- Identify GAPS - what do we need to research?
|
||||
- Think DRAMA - where's the tension in this story?
|
||||
- Consider BOTH hosts - where will Maya (technical) vs James (strategy) shine?
|
||||
- Plan DISCOVERY - what can hosts "learn" from each other during episode?
|
||||
144
skills/thebuilders-v2/templates/research-prompt.md
Normal file
144
skills/thebuilders-v2/templates/research-prompt.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# Targeted Research with Citations
|
||||
|
||||
Research the following topic according to the outline provided. Track ALL sources.
|
||||
|
||||
## Topic
|
||||
{{TOPIC}}
|
||||
|
||||
## Outline to Follow
|
||||
{{OUTLINE}}
|
||||
|
||||
---
|
||||
|
||||
## Output Format
|
||||
|
||||
Structure your research EXACTLY as follows:
|
||||
|
||||
```markdown
|
||||
# Research Report: [Topic]
|
||||
|
||||
Generated: [Date]
|
||||
Sources: [Total count]
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
[3-4 sentences: The story in brief, the key insight, why it matters]
|
||||
|
||||
---
|
||||
|
||||
## Section 1: Origin Story
|
||||
|
||||
### Key Facts
|
||||
- **Founding:** [Specific date, location, founders] [1]
|
||||
- **Initial capital:** $[Amount] from [Source] [2]
|
||||
- **Original vision:** "[Quote or paraphrase]" [3]
|
||||
- **First product:** [Description] [4]
|
||||
|
||||
### Context
|
||||
[2-3 sentences setting the scene - what was the market like? what problem existed?]
|
||||
|
||||
### Human Element
|
||||
[What makes the founders relatable? Early struggles? Quirks?]
|
||||
|
||||
### Usable Quotes
|
||||
> "[Exact quote]" — [Speaker], [Source] [5]
|
||||
|
||||
---
|
||||
|
||||
## Section 2: Key Inflection #1 - [Name the Decision]
|
||||
|
||||
### What Happened
|
||||
- **Date:** [When]
|
||||
- **Decision:** [What they chose to do]
|
||||
- **Alternatives:** [What else they could have done]
|
||||
- **Stakes:** [What they risked]
|
||||
|
||||
### The Numbers
|
||||
- [Specific metric before] → [Specific metric after] [6]
|
||||
|
||||
### Why It Mattered
|
||||
[2-3 sentences on consequences]
|
||||
|
||||
### Usable Quotes
|
||||
> "[Exact quote about this decision]" — [Speaker], [Source] [7]
|
||||
|
||||
---
|
||||
|
||||
## Section 3: Key Inflection #2 - [Name the Decision]
|
||||
[Same structure as above]
|
||||
|
||||
---
|
||||
|
||||
## Section 4: The Messy Middle
|
||||
|
||||
### Near-Death Moments
|
||||
- [Crisis #1]: [What happened, how they survived] [8]
|
||||
- [Crisis #2]: [What happened, how they survived] [9]
|
||||
|
||||
### Internal Tensions
|
||||
- [Conflict or disagreement that shaped company] [10]
|
||||
|
||||
---
|
||||
|
||||
## Section 5: Current State
|
||||
|
||||
### Position Today
|
||||
- **Valuation/Revenue:** [Latest numbers] [11]
|
||||
- **Market share:** [Position] [12]
|
||||
- **Key metrics:** [What matters for this company]
|
||||
|
||||
### Competitive Landscape
|
||||
- **Main competitors:** [Who and their position]
|
||||
- **Why winning/losing:** [Analysis]
|
||||
|
||||
### Open Questions
|
||||
- [Unresolved strategic question]
|
||||
- [Unresolved technical question]
|
||||
|
||||
---
|
||||
|
||||
## Contrarian Corner
|
||||
|
||||
### The Counter-Narrative
|
||||
[What's the bear case? What do critics say? What might go wrong?]
|
||||
|
||||
### Underreported Angle
|
||||
[What story isn't being told? What did you find that surprised you?]
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
[1] [Author/Publication]. "[Title]". [Date]. [URL if available]
|
||||
[2] ...
|
||||
[3] ...
|
||||
[Continue numbering all sources]
|
||||
|
||||
---
|
||||
|
||||
## Research Gaps
|
||||
|
||||
- [ ] [Question we couldn't answer]
|
||||
- [ ] [Source we couldn't find]
|
||||
- [ ] [Data point that would strengthen the episode]
|
||||
```
|
||||
|
||||
## Citation Rules
|
||||
|
||||
1. EVERY factual claim needs a citation number [X]
|
||||
2. Use primary sources when possible (founder interviews, SEC filings, earnings calls)
|
||||
3. Date your sources - prefer recent for current state, contemporary for historical
|
||||
4. If a fact is disputed, note the disagreement
|
||||
5. Distinguish between fact and analysis/opinion
|
||||
|
||||
## Quality Checklist
|
||||
|
||||
Before finishing, verify:
|
||||
- [ ] Every section of the outline has corresponding research
|
||||
- [ ] All claims are cited
|
||||
- [ ] At least 2 usable quotes per major section
|
||||
- [ ] Numbers are specific (not "millions" but "$4.2 million")
|
||||
- [ ] Dates are specific (not "early 2020" but "February 2020")
|
||||
- [ ] Counter-narrative is addressed
|
||||
- [ ] Sources list is complete and formatted
|
||||
101
skills/thebuilders-v2/templates/review-prompt.md
Normal file
101
skills/thebuilders-v2/templates/review-prompt.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Script Review & Refinement
|
||||
|
||||
Review the following podcast script section by section and suggest improvements.
|
||||
|
||||
## Script to Review
|
||||
{{SCRIPT}}
|
||||
|
||||
---
|
||||
|
||||
## Review Each Section
|
||||
|
||||
For each section, provide:
|
||||
|
||||
```markdown
|
||||
## Section [X]: [Name]
|
||||
|
||||
### Quality Scores
|
||||
- Hook effectiveness: [X]/10
|
||||
- Fact density: [X]/10
|
||||
- Natural dialogue: [X]/10
|
||||
- Voice consistency: [X]/10
|
||||
- Discovery moments: [X]/10
|
||||
|
||||
### What Works
|
||||
- [Specific strength]
|
||||
- [Another strength]
|
||||
|
||||
### Issues Found
|
||||
1. **[Issue type]**: [Specific problem]
|
||||
- Line: "[Quote the problematic line]"
|
||||
- Fix: "[Suggested replacement]"
|
||||
- Why: [Explanation]
|
||||
|
||||
2. **[Issue type]**: [Specific problem]
|
||||
...
|
||||
|
||||
### Voice Check
|
||||
- Maya in-character: [Yes/No] - [Note if any slip]
|
||||
- James in-character: [Yes/No] - [Note if any slip]
|
||||
|
||||
### Missing Elements
|
||||
- [ ] [Something that should be added]
|
||||
- [ ] [Another missing element]
|
||||
```
|
||||
|
||||
## Common Issues to Check
|
||||
|
||||
### Dialogue Issues
|
||||
- Monologues (turns > 5 sentences)
|
||||
- Missing reactions ("Wait, really?")
|
||||
- Unnatural transitions
|
||||
- Both hosts saying same thing
|
||||
|
||||
### Voice Consistency
|
||||
- Maya being too strategic
|
||||
- James being too technical
|
||||
- Missing signature phrases
|
||||
- Tone shifts
|
||||
|
||||
### Content Issues
|
||||
- Unsourced claims
|
||||
- Vague numbers ("millions" vs "$4.2M")
|
||||
- Dated references
|
||||
- Missing "so what?"
|
||||
|
||||
### Engagement Issues
|
||||
- No discovery moments
|
||||
- No disagreements
|
||||
- Predictable flow
|
||||
- Weak transitions between sections
|
||||
|
||||
---
|
||||
|
||||
## Final Assessment
|
||||
|
||||
```markdown
|
||||
## Overall Quality
|
||||
|
||||
**Episode Grade:** [A/B/C]
|
||||
|
||||
**Strengths:**
|
||||
1. [Top strength]
|
||||
2. [Another strength]
|
||||
|
||||
**Critical Fixes Needed:**
|
||||
1. [Must fix before TTS]
|
||||
2. [Another must-fix]
|
||||
|
||||
**Nice-to-Have Improvements:**
|
||||
1. [Would improve but not critical]
|
||||
2. [Another nice-to-have]
|
||||
|
||||
**Estimated Duration:** [X] minutes
|
||||
|
||||
**Ready for TTS:** [Yes / No - needs revision]
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
After review, provide the complete REVISED script with all fixes applied.
|
||||
Use the same format (section markers + Person tags).
|
||||
157
skills/thebuilders-v2/templates/script-prompt.md
Normal file
157
skills/thebuilders-v2/templates/script-prompt.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# The Builders Podcast Script Generation v2
|
||||
|
||||
Generate a complete podcast script with section markers and citation integration.
|
||||
|
||||
## The Hosts
|
||||
|
||||
**Maya Chen (Person1):**
|
||||
- Former software engineer, founded and sold a B2B SaaS startup
|
||||
- Lens: "How does this actually work?" - technical, mechanical, architectural
|
||||
- Skeptical of hype, wants specifics and numbers
|
||||
- Phrases: "Wait, slow down.", "Show me the numbers.", "I had to read this three times.", "That's actually clever because..."
|
||||
- NEVER says: "From a strategic perspective", "The market opportunity"
|
||||
|
||||
**James Porter (Person2):**
|
||||
- Former venture capitalist, studied business history
|
||||
- Lens: "What's the business model?" - strategy, markets, competitive dynamics
|
||||
- Synthesizer, pattern matcher, historical parallels
|
||||
- Phrases: "For context...", "This is the classic [X] playbook.", "The lesson here is...", "What a story."
|
||||
- NEVER says: "Let me explain the architecture", "The API design"
|
||||
|
||||
## Citation Integration
|
||||
|
||||
Hosts naturally reference sources:
|
||||
- "According to their Series B deck..."
|
||||
- "There's this great interview where [Founder] said..."
|
||||
- "The SEC filing actually shows..."
|
||||
- "I found this quote from [Year]..."
|
||||
|
||||
## Episode Structure
|
||||
|
||||
### SECTION 1: HOOK (Turns 1-15, ~3-4 min)
|
||||
|
||||
Use this hook:
|
||||
{{SELECTED_HOOK}}
|
||||
|
||||
Guidelines:
|
||||
- Open with the hook, let it land
|
||||
- Person2 reacts with genuine surprise/curiosity
|
||||
- Establish central question of episode
|
||||
- Tease what's coming without spoiling
|
||||
- End section with clear transition to origin
|
||||
|
||||
### SECTION 2: ORIGIN STORY (Turns 16-45, ~6-7 min)
|
||||
|
||||
Cover:
|
||||
- Who are the founders? Make them human
|
||||
- What was the genesis moment?
|
||||
- What was the market context?
|
||||
- What was their early bet/hypothesis?
|
||||
- First signs of traction or failure
|
||||
|
||||
Guidelines:
|
||||
- Maya explores technical origins
|
||||
- James provides market/strategy context
|
||||
- Include specific dates, amounts, details
|
||||
- At least one "I didn't know that" moment
|
||||
|
||||
### SECTION 3: KEY INFLECTION #1 (Turns 46-70, ~5 min)
|
||||
|
||||
Cover:
|
||||
- What decision did they face?
|
||||
- What alternatives existed?
|
||||
- What did they risk?
|
||||
- How did it play out?
|
||||
|
||||
Guidelines:
|
||||
- Build tension before revealing outcome
|
||||
- Explore the "what if they'd done X instead?"
|
||||
- Use specific numbers for before/after
|
||||
- Include a quote from the time
|
||||
|
||||
### SECTION 4: KEY INFLECTION #2 (Turns 71-90, ~4 min)
|
||||
|
||||
Cover:
|
||||
- New challenge or opportunity
|
||||
- How they adapted/pivoted
|
||||
- Key insight they gained
|
||||
|
||||
Guidelines:
|
||||
- Connect to first inflection
|
||||
- Show evolution of thinking
|
||||
- Maya: technical implications
|
||||
- James: strategic implications
|
||||
|
||||
### SECTION 5: MESSY MIDDLE (Turns 91-105, ~3 min)
|
||||
|
||||
Cover:
|
||||
- Near-death moment(s)
|
||||
- Internal conflicts
|
||||
- What almost broke them
|
||||
|
||||
Guidelines:
|
||||
- Don't glorify - show real struggle
|
||||
- Include specific stakes ("6 months of runway")
|
||||
- One host can play devil's advocate
|
||||
|
||||
### SECTION 6: NOW (Turns 106-120, ~3 min)
|
||||
|
||||
Cover:
|
||||
- Current position and metrics
|
||||
- Competitive landscape
|
||||
- Open questions
|
||||
|
||||
Guidelines:
|
||||
- Timeless framing (position, not news)
|
||||
- Acknowledge uncertainty about future
|
||||
- Set up takeaways
|
||||
|
||||
### SECTION 7: TAKEAWAYS (Turns 121-130, ~2-3 min)
|
||||
|
||||
Cover:
|
||||
- Key lesson(s)
|
||||
- Framework or principle
|
||||
- Final memorable thought
|
||||
|
||||
Guidelines:
|
||||
- Both hosts contribute insights
|
||||
- Connect back to hook/central question
|
||||
- End with forward-looking thought
|
||||
- Final line should resonate
|
||||
|
||||
## Format Rules
|
||||
|
||||
1. Use `<Person1>` and `<Person2>` XML tags
|
||||
2. Each turn: 2-5 sentences (conversation, not monologue)
|
||||
3. Add section markers: `<!-- SECTION X: NAME -->`
|
||||
4. Include discovery moments in every section
|
||||
5. NO news references - timeless only
|
||||
6. Use SPECIFIC facts from research
|
||||
7. Both hosts should learn during conversation
|
||||
8. Minimum 130 turns total
|
||||
|
||||
## Section Marker Format
|
||||
|
||||
```
|
||||
<!-- SECTION 1: HOOK -->
|
||||
<Person1>The email arrived at 2 AM...</Person1>
|
||||
...
|
||||
|
||||
<!-- SECTION 2: ORIGIN -->
|
||||
<Person1>Okay, so let's go back to the beginning...</Person1>
|
||||
...
|
||||
```
|
||||
|
||||
## Research Material
|
||||
|
||||
{{RESEARCH}}
|
||||
|
||||
---
|
||||
|
||||
## Selected Hook
|
||||
|
||||
{{HOOK}}
|
||||
|
||||
---
|
||||
|
||||
Generate the complete script now. Start with `<!-- SECTION 1: HOOK -->` followed by `<Person1>`.
|
||||
Reference in New Issue
Block a user