Skip to content

Repository files navigation

podcraft-ai

End-to-end AI pipeline that researches a topic, writes a two-host script, synthesizes voices, and exports a broadcast-ready podcast episode.

homepage-podcastai

waveform-card


How It Works

PodCraft AI runs four sequential stages to produce a complete podcast episode from a single topic prompt:

Topic (string)
    │
    ▼
Stage 1 — Research Agent
    Searches the web via Tavily, synthesizes findings into a structured brief
    │  episode_N_brief.json
    ▼
Stage 2 — Script Generator
    Sends the brief to Claude, receives a two-host JSON script
    │  episode_N_script.json
    ▼
Stage 3 — Audio Assembler
    Calls ElevenLabs TTS for each speaker turn, overlays background music,
    assembles and normalizes the final mix
    │  episode_N.mp3
    ▼
Stage 4 — Metadata Packager
    Generates show notes and tags via Claude, writes RSS sidecar JSON,
    optionally uploads to Buzzsprout
    │  episode_N_meta.json
    ▼
episodes/

Each stage saves its output to episodes/ so any stage can be re-run independently using --from-stage.


Technology Stack

Layer Tool / Service
LLM Anthropic Claude (claude-sonnet-4-5)
Agent orchestration LangChain ReAct agent
Web search Tavily API
Text-to-speech ElevenLabs (eleven_turbo_v2)
Audio assembly pydub + ffmpeg
Backend API FastAPI + uvicorn
Frontend React 18 + TypeScript + Vite
RSS feed RSS 2.0 + iTunes namespace (built-in, no library)
Env management python-dotenv
HTTP client requests
Testing pytest
Linting ruff

Project Structure

podcraft-ai/
├── src/
│   ├── pipeline.py            # Entry point — orchestrates all four stages
│   ├── research_agent.py      # Stage 1 — LangChain ReAct + Tavily search
│   ├── script_generator.py    # Stage 2 — Claude → two-host JSON script
│   ├── audio_assembler.py     # Stage 3 — ElevenLabs TTS + pydub assembly
│   ├── metadata_packager.py   # Stage 4 — Claude show notes + sidecar JSON
│   └── utils/
│       ├── llm.py             # Shared Claude API client
│       ├── logger.py          # Structured logging
│       └── validators.py      # JSON schema validators
├── prompts/
│   ├── research_prompt.txt    # Stage 1 system prompt
│   └── script_prompt.txt      # Stage 2 host personas + segment structure
├── web/                       # React frontend
│   └── src/
│       ├── App.tsx
│       ├── api.ts             # Fetch wrappers for all API routes
│       ├── components/
│       │   ├── EpisodeList.tsx
│       │   └── RunPipeline.tsx
│       └── index.css
├── episodes/                  # Output directory (mp3 + JSON sidecars)
├── music/                     # Royalty-free background tracks
├── server.py                  # FastAPI server + SSE log streaming
├── requirements.txt
└── .env.example

API Routes

Episodes

Method Route Description
GET /api/episodes List all episodes that have a sidecar _meta.json
DELETE /api/episodes/{episode_num} Delete the mp3, meta, brief, and script files for an episode
GET /episodes/{filename} Stream an episode mp3 (static mount)

Pipeline

Method Route Description
POST /api/pipeline/run Start a pipeline run; returns { run_id }
GET /api/pipeline/run/{run_id}/stream SSE stream of log lines for a run
GET /api/pipeline/run/{run_id}/status Poll the current status of a run

POST /api/pipeline/run request body:

{
  "topic": "string",
  "episode_num": 1,
  "dev_mode": false
}

Feed

Method Route Description
GET /feed.xml RSS 2.0 + iTunes podcast feed generated from all episode metadata

System Architecture

Backend (server.py)

FastAPI serves both the REST API and static episode files. Pipeline runs execute in background threads. Log lines are forwarded from Python's logging module into a queue.Queue per run, then streamed to the browser via Server-Sent Events (SSE).

Browser
  │  POST /api/pipeline/run
  ▼
FastAPI → spawns Thread → run_pipeline()
                              │ logger.info(...)
                              ▼
                         _QueueHandler → Queue
                              │
  Browser ←── SSE stream ─────┘
  GET /api/pipeline/run/{id}/stream

Frontend (web/)

React SPA built with Vite. In development, Vite proxies /api, /episodes, and /feed.xml to the FastAPI server at localhost:8000.

  • EpisodeList — fetches episodes on mount and after each pipeline run; supports in-browser playback with a seekable progress bar
  • RunPipeline — submits the pipeline form and opens an EventSource to stream live log output into the console panel

Audio Assembly (src/audio_assembler.py)

Each speaker turn is synthesized individually and written to a temp file, loaded into pydub, then deleted. Segments are assembled with configurable silence gaps between turns (300ms) and segments (800ms). Background music runs only under the intro and wrap segments — the main segment is dialogue-only. Music is sliced to exactly match the segment length and faded within that window (fade-out on intro, fade-in on wrap). The final mix is normalized before export at 192k bitrate.

Pipeline Resume (src/pipeline.py)

The --from-stage flag (1–4) allows resuming a failed run without re-calling upstream APIs. Stages before from_stage load their output from saved JSON files in episodes/ rather than making API calls.


Host Personas

Host Voice Persona
Aria (HOST_A) ElevenLabs "Aria" Warm, curious, drives the narrative, accessible language, audience surrogate
Alex (HOST_B) ElevenLabs "Alex - Professional" Analytical, slightly skeptical, adds context and data, grounds enthusiasm with evidence

Script segments follow a fixed structure: intromainwrap.


Setup

Requirements

  • Python 3.11+
  • Node.js 18+
  • ffmpeg on PATH

Environment Variables

Copy .env.example to .env and fill in:

ANTHROPIC_API_KEY=
ELEVENLABS_API_KEY=
TAVILY_API_KEY=
ELEVENLABS_VOICE_ID_HOST_A=    # Aria voice ID from ElevenLabs VoiceLab
ELEVENLABS_VOICE_ID_HOST_B=    # Alex voice ID from ElevenLabs VoiceLab
PODCAST_BASE_URL=              # Public URL for RSS feed enclosure links
PODCAST_TITLE=
PODCAST_AUTHOR=
PODCAST_EMAIL=
PODCAST_DESCRIPTION=
BUZZSPROUT_API_KEY=            # Optional — enables automatic upload
BUZZSPROUT_PODCAST_ID=         # Optional

Install

pip install -r requirements.txt
cd web && npm install

Run

# Terminal 1 — FastAPI backend
python server.py

# Terminal 2 — Vite frontend
cd web && npm run dev

UI → http://localhost:5173 | API → http://localhost:8000 | RSS → http://localhost:8000/feed.xml


CLI Usage

# Full pipeline run
python -m src.pipeline --topic "AI in journalism" --episode 1

# Dev mode (only 2 TTS turns — conserves ElevenLabs quota)
python -m src.pipeline --topic "AI in journalism" --episode 1 --dev

# Resume from a specific stage (skips earlier stages, loads saved JSON)
python -m src.pipeline --topic "AI in journalism" --episode 1 --from-stage 3

Development Notes

  • Always use --dev for test runs to avoid burning ElevenLabs quota (10k chars/month on free tier)
  • Voice IDs must be added to your ElevenLabs VoiceLab before the API can use them — copy the ID from the three-dot menu on the VoiceLab entry
  • server.py only calls load_dotenv() at startup — restart the server after editing .env
  • Run the linter: ruff check src/
  • Run tests: pytest tests/ -v

License

Private — © 2026 Nathan Curtis. All rights reserved.

This repository is public for viewing purposes only. No permission is granted to copy, modify, or redistribute without explicit written permission.

About

End-to-end AI pipeline that researches a topic, writes a two-host script, synthesizes voices, and exports a broadcast-ready podcast episode.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages