---
name: personal-podcast-skill
description: Turn chat context, Markdown, plans, notes, reports, or other source material into a direct spoken-word podcast episode, generate AI narration with OpenAI text-to-speech, maintain an RSS feed, and publish the audio, artwork, and feed to Cloudflare R2. Use when a user asks to make an audio briefing, convert material into a podcast or private feed, publish a new personal podcast episode, or update an existing self-hosted podcast feed.
---

# Personal Podcast Publisher

Create concise spoken scripts, render them with OpenAI TTS, and publish an Apple-compatible RSS feed to Cloudflare R2. Keep creative writing flexible; use the bundled scripts for configuration, audio rendering, RSS generation, uploads, and verification.

## Resolve paths

Treat the directory containing this `SKILL.md` as `SKILL_ROOT`. Set that shell variable to the absolute directory, or replace it with the absolute path in every example. Invoke scripts with absolute paths when the current working directory is elsewhere.

Use the configuration path in `PODCAST_CONFIG` when set. Otherwise use `~/.config/personal-podcast/config.json`.

## Workflow

Track this checklist and do not skip validation gates:

- [ ] Load or create configuration
- [ ] Identify the source and episode scope
- [ ] Write the spoken script and metadata
- [ ] Obtain approval when configured
- [ ] Run local and remote preflight checks
- [ ] Render and publish
- [ ] Verify the public feed, artwork, and audio

### 1. Load or create configuration

Run:

```bash
node "$SKILL_ROOT/scripts/doctor.mjs"
```

If configuration is missing, read `references/configuration.md` and perform its guided first-run setup. Do not present the user with a long form or ask for values that can be discovered locally.

Work through setup in this order:

1. Check OpenAI credential availability without printing the credential.
2. Check whether Wrangler is installed and authenticated.
3. List accessible R2 buckets and the custom domains connected to the selected bucket.
4. Find or request square cover artwork.
5. Propose show metadata from the conversation and ask the user to confirm it.

Explain each missing prerequisite and give one concrete next action. A bucket name and a production custom-domain URL are both required: the bucket stores the files, while the domain provides stable public feed, artwork, and audio URLs. If several buckets or domains are available, present the choices instead of asking the user to type them. If no bucket exists, ask permission before creating one. Never create a bucket, attach a domain, or change DNS without explicit approval.

Offer these defaults instead of asking about every field:

- R2 prefix: slug of the podcast title
- Voice: `cedar`
- TTS model: `gpt-4o-mini-tts`
- Language: `en-us`
- Category: `Technology`
- Require script approval before paid rendering and publishing: `true`

Never ask the user to paste an API key or Cloudflare token into chat. For OpenAI, recommend the bundled hidden-input helper:

```bash
"$SKILL_ROOT/scripts/store-openai-key.sh"
```

On macOS with Xcode Command Line Tools it stores and verifies the complete key in the user's Keychain. On other systems it saves a protected file at `~/.config/personal-podcast/openai-api-key`. The scripts also accept `OPENAI_API_KEY` or `OPENAI_API_KEY_FILE` as explicit overrides. For Cloudflare, recommend `wrangler login`; a headless environment may use `CLOUDFLARE_API_TOKEN`.

Create a temporary answers JSON based on `assets/podcast.config.example.json`, then run:

```bash
node "$SKILL_ROOT/scripts/setup.mjs" --answers /absolute/path/to/answers.json
node "$SKILL_ROOT/scripts/doctor.mjs" --remote
```

The remote doctor check must authenticate the configured OpenAI credential without rendering audio. Do not proceed to TTS when it reports an authentication failure.

Do not ask whether the user approves “the current script” during setup. Script approval belongs to step 4, after an episode script has actually been drafted.

Read `references/configuration.md` only during setup, reconfiguration, or credential troubleshooting.

### 2. Identify source and episode scope

Use the source named by the user. If they say “this,” use the relevant conversation or current document. Ask one concise question only when the source or intended episode scope is genuinely ambiguous.

Do not silently broaden the material. Preserve facts, decisions, caveats, ordering, and actionable detail. Remove formatting artifacts rather than content.

### 3. Write the spoken script and metadata

Read `references/script-style.md` before drafting.

Create two UTF-8 files in a dedicated draft directory:

- `script.txt`: narration only, with paragraphs and no Markdown formatting.
- `episode.json`: copy the shape from `assets/episode.example.json` and fill every required field.

Keep the delivery direct. Do not invent hosts, banter, fake interviews, advertisements, theme music, or podcast theater unless the user explicitly requests them.

Include a natural disclosure in the episode description that the narration uses an AI-generated voice. The publishing script also enforces this disclosure.

### 4. Respect the approval setting

If `workflow.require_script_approval` is `true`, show the episode title, description, approximate word count, estimated duration at 145 words per minute, and the script path. Ask for approval before calling TTS or uploading.

Skip this pause when the setting is `false` or the user explicitly pre-approved rendering and publishing in the current request.

### 5. Run preflight checks

Before a paid TTS call or live upload, run:

```bash
node "$SKILL_ROOT/scripts/doctor.mjs" --remote
node "$SKILL_ROOT/scripts/publish.mjs" \
  --script /absolute/path/to/script.txt \
  --metadata /absolute/path/to/episode.json \
  --dry-run
```

Fix all reported errors. Do not work around missing secrets by printing them, embedding them in commands, or writing them into configuration.

### 6. Render and publish

Run:

```bash
node "$SKILL_ROOT/scripts/publish.mjs" \
  --script /absolute/path/to/script.txt \
  --metadata /absolute/path/to/episode.json
```

The script renders chunked MP3 audio, uploads immutable versioned audio and artwork, updates persistent episode state, generates RSS, uploads the feed last, and verifies public delivery. Re-running the same script and metadata is idempotent unless `--force` is supplied.

To publish pre-rendered audio, add `--audio /absolute/path/to/episode.mp3`.

### 7. Verify and report

Require all of these before declaring success:

- Feed returns HTTP 200 and parses as XML.
- Artwork returns HTTP 200 with an image content type.
- Audio returns HTTP 200 to `HEAD` and HTTP 206 to a byte-range request.
- The public feed contains the new stable GUID and enclosure URL.

Report the episode title, duration, feed URL, audio URL, artwork URL, and local draft/output paths. If podcast apps have not refreshed yet, explain that feed and artwork updates can take time; do not purge caches unless the user asks.

## Updating show settings

Use `scripts/setup.mjs --answers ... --force` to update non-secret configuration. Changing cover art causes the publisher to create a content-hashed filename and update the feed, avoiding stale artwork caches.

Do not change the public feed URL, existing episode GUIDs, or enclosure URLs without explicit user authorization. These identifiers are durable podcast state.

## Failure handling

- If TTS fails, keep drafts and any completed chunks; do not publish a partial episode.
- If an asset upload fails, do not upload the feed.
- If feed upload fails, restore local feed state from the automatic backup and report the unreferenced uploaded assets.
- If verification fails, report the exact URL and check that the R2 bucket uses an active production custom domain. `r2.dev` is for development, not production delivery.
- If artwork changes, always publish it under a new filename.

Read `references/publishing.md` only for RSS, Apple Podcasts, R2, caching, or recovery issues.
