Skip to main content
Glama
brienalkire

podcast-generator

by brienalkire
README.md
# podcast_generator

Converts markdown podcast scripts into spoken-word audio files, using
[`voxport`](https://github.com/brienalkire/voxport)'s `KokoroDirectTTS` for local, offline-capable
text-to-speech. No API key required.

This README is a quick start. See [MANUAL.md](MANUAL.md) for the full
guide: script-writing conventions, complete CLI reference, the voice
catalog, and troubleshooting.

## Installation

Requires Python 3.10+ and Windows for the scheduled mode below (the CLI
itself is plain Python).

```
git clone https://github.com/brienalkire/podcast_generator.git
cd podcast_generator
python -m venv .venv
.venv\Scripts\activate          # Windows (bash: source .venv/Scripts/activate)
pip install -r requirements.txt
```

This installs [`voxport`](https://pypi.org/project/voxport/) from PyPI with
its `kokoro` extra (Kokoro and torch, about 2 GB), plus soundfile,
python-dotenv and the MCP SDK. If you're also working on voxport itself,
clone it next to this repo and run `pip install -e "../voxport[kokoro]"`
afterwards to use your checkout instead.

Check the install:

```
python generator.py --version
```

Not installed by this repo:

- **ffmpeg**: needed for MP3 output unless your `soundfile`/libsndfile
  build has native MP3 support. Put it on `PATH`. WAV output needs neither.
- **Kokoro model weights and spaCy's `en_core_web_sm`**: downloaded
  automatically on the first run. On a firewalled machine, see
  "Running behind a corporate firewall" in
  `instructions_creating_podcast_generator.md` (or `voxport`'s README) for
  an offline Hugging Face cache, and run
  `python -m spacy download en_core_web_sm` once where network access is
  available.

Optionally, copy `.env.example` to `.env` to point the inputs/outputs/archive
folders somewhere other than this repo (see "Folder configuration" below).

## Usage

### Writing a script

A script is a markdown file. Plain paragraphs are spoken. Headers,
`**Key:** value` metadata lines, horizontal rules, and italic-only lines
are stripped, so you can label sections and add notes without them being
read aloud:

```markdown
# My Episode Title

**Voice:** af_heart

## OPENING

Welcome back to the show. Today we're talking about...

## CLOSING

That's all for this episode. Thanks for listening.

---

*Source: some-article.com, September 2026.*
```

Only the two paragraphs are spoken. An italic-only line after the prose
starts ends the script, and everything after it is dropped. Keep those
lines for trailing notes. See [MANUAL.md](MANUAL.md#writing-a-script) for
the full rules.

### Running it

```
python generator.py                                   # batch: every script in inputs/
python generator.py --voice af_heart --speed 1.1      # batch, overriding voice and speed
python generator.py my-script.md --format wav --output episode01.wav   # one file
```

**Batch mode** (no file argument) converts every `.md` script in the inputs
folder (create it first; the default is `inputs/` next to `generator.py`)
and writes `<script-name>.mp3` to the outputs folder. Each script that
converts successfully is moved into the archive folder, so re-running
doesn't reprocess it. A script that fails moves to
`<archive folder>/failed/` with a `.error.txt` note, so it isn't retried
forever.

**Single-file mode** converts just the named file. `--output` only applies
here.

| Option | Meaning |
|---|---|
| `--voice VOICE` | Kokoro voice for the whole run. Overrides a script's `**Voice:**` line. Default `af_heart`. |
| `--speed FLOAT` | Speaking rate (default `1.0`; `0.9` slower, `1.1` faster). |
| `--format {mp3,wav}` | Output format (default `mp3`). |
| `--output PATH` | Output file (single-file mode only). |
| `--lang CODE` | Accent override; normally inferred from the voice name. |
| `--version` | Print the version and exit. |

Voice precedence: `--voice`, then the script's `**Voice:**` line, then
`af_heart`. The full voice catalog and troubleshooting are in
[MANUAL.md](MANUAL.md).

## Folder configuration

Defaults to `inputs/`, `outputs/`, `script_archive/` next to `generator.py`.
Override them with the Podcast Watch app (see below), or by copying
`.env.example` to `.env` and setting
`PODCAST_INPUTS_DIR`, `PODCAST_OUTPUTS_DIR`, `PODCAST_ARCHIVE_DIR` — e.g.
pointed at a folder mirrored by a cloud sync client (Sync.com, Dropbox,
etc.), so a script dropped in remotely gets picked up and the finished
audio syncs back out. **The same folders are used for both manual and
scheduled runs** — there's no separate "watch mode" config.

A typical setup points `.env` at a Sync.com/Dropbox/OneDrive-mirrored
folder (e.g. `C:\Users\yourname\Sync\podcasts\{inputs,outputs,
script_archive}`) — drop a script into `inputs/` from any device your
sync client runs on, and the finished audio syncs back out through
`outputs/`.

## Scheduled / "drop folder" mode

There's no separate watcher script. Since a run archives what it processes,
running the exact same batch command (`python generator.py`, no arguments)
on a recurring schedule *is* watch mode — each run picks up whatever's new
in the inputs folder since last time. An empty inputs folder exits 0
almost immediately (job discovery happens before any heavy import, and an
empty queue isn't treated as an error), so scheduling this every 10-15
minutes costs essentially nothing when there's nothing to do.

Only one conversion runs at a time: a run that starts while another is
still converting leaves the queue to it and exits.

The easiest way to set up the schedule is the **Set up watch** button in
the Podcast Watch app (below). It registers a Windows scheduled task named
`PodcastGeneratorWatch` for your user (no admin rights or stored password;
runs while you're logged in, including on battery) that runs
`.venv\Scripts\pythonw.exe generator.py` every 10 minutes, or whatever
interval you pick. `pythonw.exe` means no console window flashes up; the
output goes to `generator.log` in this folder instead.

Equivalently, from PowerShell:

```powershell
$repo = 'C:\path\to\podcast_generator'
$Action = New-ScheduledTaskAction -Execute "$repo\.venv\Scripts\pythonw.exe" -Argument 'generator.py' -WorkingDirectory $repo
$Trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 10) -RepetitionDuration (New-TimeSpan -Days 3650)
$Settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -MultipleInstances IgnoreNew
Register-ScheduledTask -TaskName "PodcastGeneratorWatch" -Action $Action -Trigger $Trigger -Settings $Settings
```

To remove it: `Unregister-ScheduledTask -TaskName PodcastGeneratorWatch`.
Manual `python generator.py` runs work whether the watch is on or off.

### Turning it on/off — the Podcast Watch app

A desktop shortcut, **"Podcast Watch"**, opens a small window
(`scripts/podcast-watch.ps1`, no install needed) for day-to-day control,
so you don't need PowerShell or Task Scheduler:

- **Set up watch** (shown until the task exists) registers the
  `PodcastGeneratorWatch` task, as described above.
- **Turn ON / Turn OFF** enables or disables the task and shows its
  current state. **Check every** changes how often it runs.
- **When this app opens** can leave the watch as it was (the default), or
  always turn it on or always turn it off when the window opens. This is
  saved to `.env` as `PODCAST_WATCH_ON_OPEN=on|off`.
- **Folders** shows the inputs, outputs, and archive folders in use.
  **Browse...** picks a new one and saves it to `.env`. **Use default**
  removes the override, so the repo's own `inputs/`, `outputs/`, and
  `script_archive/` are used. **Open** opens the folder in Explorer and
  creates it if it doesn't exist yet.

The app only rewrites the keys it changes. Other `.env` lines and comments
are kept. Folder changes take effect on the next run, scheduled or manual.
See [MANUAL.md](MANUAL.md#the-podcast-watch-app) for a screenshot and
more detail.

To create the shortcut on another machine:

```powershell
$desktop = [Environment]::GetFolderPath("Desktop")
$repo = "C:\path\to\podcast_generator"
$wsh = New-Object -ComObject WScript.Shell
$lnk = $wsh.CreateShortcut("$desktop\Podcast Watch.lnk")
$lnk.TargetPath = "powershell.exe"
$lnk.Arguments = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$repo\scripts\podcast-watch.ps1`""
$lnk.WorkingDirectory = $repo
$lnk.IconLocation = "shell32.dll,238"
$lnk.Save()
```

## Using it from Claude Desktop (MCP)

`mcp_server.py` is an [MCP](https://modelcontextprotocol.io) server, so an
AI app such as Claude Desktop can make podcasts directly: write a script,
queue it, convert it, and check on it, all from the chat. It runs locally
and uses the same folders and `.env` as everything else.

Add it to Claude Desktop's config (Settings → Developer → Edit Config,
which opens `claude_desktop_config.json`), then restart Claude Desktop:

```json
{
  "mcpServers": {
    "podcast-generator": {
      "command": "C:\\path\\to\\podcast_generator\\.venv\\Scripts\\python.exe",
      "args": ["C:\\path\\to\\podcast_generator\\mcp_server.py"]
    }
  }
}
```

Any other MCP client that can launch a local (stdio) server works the same
way. See [MANUAL.md](MANUAL.md#using-it-from-claude-desktop-mcp) for the
tools and an example conversation.

## Notes

- `instructions_creating_podcast_generator.md` is a narrative guide to
  writing scripts and the end-to-end workflow (by hand or drop folder),
  including script-writing style advice and corporate-firewall setup.
- `inputs/`, `outputs/`, and `script_archive/` (the local defaults) are
  gitignored — personal script drafts and generated audio aren't toolkit
  code. The real, cloud-synced versions of these folders live outside this
  repo entirely (see `.env`).