
Podcastfy Clawdbot
- 2 installs
- 16 repo stars
- Updated January 31, 2026
- win4r/podcastfy-clawdbot-skill
Helps with ai & agent building tasks.
About
podcastfy-clawdbot is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- podcastfy-clawdbot
- AI & Agent Building
- AI-coding skill
Podcastfy Clawdbot by the numbers
- 2 all-time installs (skills.sh)
- Ranked #13,958 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 27, 2026 (Skillselion catalog sync)
npx skills add https://github.com/win4r/podcastfy-clawdbot-skill --skill podcastfy-clawdbotAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 16 |
| Last updated | January 31, 2026 |
| Repository | win4r/podcastfy-clawdbot-skill ↗ |
What it does
Helps with ai & agent building tasks.
Files
Podcastfy (Clawdbot)
Generate a podcast-style audio conversation (MP3) from a URL (or multiple URLs) using podcastfy.
This skill provides a wrapper script that:
- creates/uses a local venv (
{baseDir}/.venv) - installs/updates
podcastfy - runs Podcastfy with Gemini for transcript generation and Edge for TTS
One-time setup
1) Ensure ffmpeg is installed on the host.
Ubuntu/Debian:
sudo apt-get update && sudo apt-get install -y ffmpeg2) Provide a Gemini API key:
Set env var GEMINI_API_KEY (recommended), or create a project .env and export it before running.
Example keys file: {baseDir}/references/env.example
Quick start
Generate an MP3 from a single URL:
cd {baseDir}
export GEMINI_API_KEY="..."
./scripts/podcastfy_generate.py --url "https://example.com/article"Multiple URLs:
cd {baseDir}
./scripts/podcastfy_generate.py --url "https://a" --url "https://b"Long-form:
cd {baseDir}
./scripts/podcastfy_generate.py --url "https://example.com/long" --longformOutput
The script writes outputs under:
{baseDir}/output/audio/(MP3){baseDir}/output/transcripts/(transcript)
Podcastfy prints the final MP3 path on success.
Optional tuning
PODCASTFY_LLM_MODEL(default:gemini-1.5-flash)PODCASTFY_EDGE_VOICE_Q(default:en-US-JennyNeural)PODCASTFY_EDGE_VOICE_A(default:en-US-EricNeural)
Automation / reliability tips (important)
Prefer RSS feeds over browser automation for cron jobs
In isolated cron jobs, avoid relying on a system browser (Chrome/Chromium) or extra Python deps (e.g. bs4). For TechCrunch categories, prefer the RSS feed:
- https://techcrunch.com/category/artificial-intelligence/feed/
This reduces breakage from:
- “No supported browser found …”
- missing site parsing dependencies in the cron runtime
Validate MP3 output (avoid 0-second audio)
We observed podcastfy can occasionally produce an MP3 that is effectively empty/truncated (e.g., a ~261-byte file), which shows up as 0s in Telegram.
This wrapper now validates MP3 output and will automatically fall back to edge-tts synthesis from the latest transcript when the MP3 is invalid.
Safety / workflow
- Prefer “draft-first”: tell the user what will be generated (language/length) before running.
- Never paste API keys into chat logs.
# Secrets
.env
.env.*
# Virtualenv
.venv/
# Output artifacts
output/
*.mp3
*.wav
*.m4a
# Python
__pycache__/
*.pyc
# OS
.DS_Store
# Local overrides for Podcastfy
# (Used by the Clawdbot wrapper via `config=`)
website_extractor:
# TechCrunch (and similar sites) can take longer to reach networkidle.
# Default in podcastfy is 10 seconds.
timeout: 60
podcastfy-clawdbot-skill
A Clawdbot skill that generates a podcast-style MP3 from one or more URLs using the open-source Podcastfy project.
It is designed to be used inside a Clawdbot deployment and provides a wrapper script that:
- Creates/uses a local Python venv
- Installs/updates
podcastfy - Uses Gemini for script generation (LLM)
- Uses Edge TTS for speech synthesis (no paid TTS API required)
Note: This repository intentionally does not include any generated audio/transcripts or any secrets.
---
Features
- Generate a single MP3 from a single article URL
- Generate one MP3 from multiple URLs (e.g., 3 articles → 1 combined episode)
- Compatible with Clawdbot scheduled jobs (cron)
- Safer defaults:
.env,.venv, andoutput/are ignored
---
Requirements
- Linux/macOS (tested in Linux)
- Python 3.10+ (tested with Python 3.12)
ffmpeg- A valid
GEMINI_API_KEY(Gemini API)
Install ffmpeg (Ubuntu/Debian):
sudo apt-get update && sudo apt-get install -y ffmpeg---
Setup
1) Create a .env
Create a .env file in the skill directory:
GEMINI_API_KEY=YOUR_KEY_HERE
PODCASTFY_LLM_MODEL=gemini-2.5-flashOptional (Edge TTS voices):
PODCASTFY_EDGE_VOICE_Q=en-US-JennyNeural
PODCASTFY_EDGE_VOICE_A=en-US-EricNeural2) Run
Single URL:
cd skills/podcastfy-clawdbot
./scripts/podcastfy_generate.py --url "https://example.com/article"Multiple URLs:
cd skills/podcastfy-clawdbot
PODCASTFY_LLM_MODEL=gemini-2.5-flash \
./scripts/podcastfy_generate.py \
--url "https://example.com/a" \
--url "https://example.com/b" \
--url "https://example.com/c"---
Output
On success, the script prints the generated MP3 path.
Typical output locations:
output/audio/*.mp3output/transcripts/*.txt
---
Notes / Troubleshooting
- If Gemini returns
429withlimit: 0, it usually means your project/key has no usable quota (billing/quota not enabled or free-tier not available). Check your Gemini API rate-limit page. - Some websites keep connections open and can cause Playwright timeouts; the wrapper/config is tuned to be more tolerant, but some pages may still fail.
---
License
Add a license if you plan to publish/redistribute.
---
中文说明(Chinese)
这是一个用于 Clawdbot 的技能(skill),可以把一个或多个网页链接生成「播客风格」的 MP3。
该仓库提供一个包装脚本,主要功能:
- 自动创建/使用 Python 虚拟环境
- 安装/更新
podcastfy - 使用 Gemini 生成播客对话稿(LLM)
- 使用 Edge TTS 合成语音(无需付费 TTS API)
注意:本仓库不会提交任何密钥、.env、虚拟环境、以及生成的音频/转写文件。---
功能
- 单链接生成一条 MP3
- 多链接合并生成一条 MP3(例如 3 篇文章合成 1 期播客)
- 可配合 Clawdbot 的定时任务(cron)每日自动生成并推送
- 默认做安全清理:忽略
.env/.venv/output/
---
环境要求
- Linux/macOS(主要在 Linux 测试)
- Python 3.10+(测试环境为 Python 3.12)
ffmpeg- 可用的
GEMINI_API_KEY
Ubuntu/Debian 安装 ffmpeg:
sudo apt-get update && sudo apt-get install -y ffmpeg---
配置与运行
1)创建 .env
在 skill 目录创建 .env:
GEMINI_API_KEY=你的Key
PODCASTFY_LLM_MODEL=gemini-2.5-flash可选:指定 Edge TTS 声音:
PODCASTFY_EDGE_VOICE_Q=en-US-JennyNeural
PODCASTFY_EDGE_VOICE_A=en-US-EricNeural2)运行
单链接:
cd skills/podcastfy-clawdbot
./scripts/podcastfy_generate.py --url "https://example.com/article"多链接(合并生成一条播客):
cd skills/podcastfy-clawdbot
PODCASTFY_LLM_MODEL=gemini-2.5-flash \
./scripts/podcastfy_generate.py \
--url "https://example.com/a" \
--url "https://example.com/b" \
--url "https://example.com/c"---
输出
成功后脚本会打印 MP3 输出路径,通常在:
output/audio/*.mp3output/transcripts/*.txt
---
常见问题
- 如果 Gemini 报错
429且包含limit: 0,通常是因为该项目/Key 当前没有可用配额(未启用计费或 free-tier 不可用)。请到 Gemini API 的 rate limit 页面确认配额状态。 - 某些网站会让 Playwright 一直等待(网络连接不断开),可能导致抓取超时;此仓库已做一定容错,但仍可能遇到个别页面失败。
# Required
GEMINI_API_KEY=your_gemini_api_key
# Optional tuning
# PODCASTFY_LLM_MODEL=gemini-1.5-flash
# PODCASTFY_EDGE_VOICE_Q=en-US-JennyNeural
# PODCASTFY_EDGE_VOICE_A=en-US-EricNeural
#!/usr/bin/env python3
"""Generate a podcast MP3 from one or more URLs using podcastfy.
Goals (Clawdbot skill wrapper):
- Provide a single command that takes URLs and outputs an MP3 path.
- Use Gemini for transcript generation via GEMINI_API_KEY.
- Use Edge TTS (podcastfy's built-in edge support) for audio.
This script:
- Ensures a local venv exists and installs podcastfy.
- Writes a temporary conversation config that sets Edge voices + output dirs.
- Runs Podcastfy via the Python API.
- Verifies the produced MP3 is playable (non-trivial size + ffprobe duration).
- If the MP3 is invalid/truncated, re-synthesizes audio from the latest transcript
using edge-tts as a fallback.
Usage:
./podcastfy_generate.py --url https://example.com/article
./podcastfy_generate.py --url https://a --url https://b --longform
Env:
GEMINI_API_KEY (required)
PODCASTFY_LLM_MODEL (optional; default: gemini-1.5-flash)
PODCASTFY_EDGE_VOICE_Q (optional; default: en-US-JennyNeural)
PODCASTFY_EDGE_VOICE_A (optional; default: en-US-EricNeural)
Notes:
- Requires ffmpeg available on PATH (also used by podcastfy).
"""
from __future__ import annotations
import argparse
import os
import shutil
import subprocess
import sys
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent
SKILL_DIR = BASE_DIR.parent
VENV_DIR = SKILL_DIR / ".venv"
PY = VENV_DIR / "bin" / "python"
PIP = VENV_DIR / "bin" / "pip"
EDGE_TTS = VENV_DIR / "bin" / "edge-tts"
def ensure_ffmpeg() -> None:
if shutil.which("ffmpeg"):
return
raise SystemExit(
"ffmpeg not found on PATH. Install it first (Ubuntu/Debian): sudo apt-get update && sudo apt-get install -y ffmpeg"
)
def ensure_venv() -> None:
if PY.exists() and PIP.exists():
return
subprocess.run(["python3", "-m", "venv", str(VENV_DIR)], check=True)
subprocess.run([str(PIP), "install", "--upgrade", "pip"], check=True)
def ensure_deps() -> None:
# Pin loosely; let pip resolve compatible versions.
subprocess.run([str(PIP), "install", "-U", "podcastfy", "playwright"], check=True)
# Podcastfy's website extractor may use Playwright. Ensure a browser is installed.
try:
subprocess.run([str(PY), "-m", "playwright", "install", "chromium"], check=True)
except Exception as e:
raise SystemExit(f"Failed to install Playwright Chromium: {e}")
def write_conversation_config(out_dir: Path) -> Path:
out_dir.mkdir(parents=True, exist_ok=True)
(out_dir / "audio").mkdir(parents=True, exist_ok=True)
(out_dir / "transcripts").mkdir(parents=True, exist_ok=True)
voice_q = os.getenv("PODCASTFY_EDGE_VOICE_Q", "en-US-JennyNeural")
voice_a = os.getenv("PODCASTFY_EDGE_VOICE_A", "en-US-EricNeural")
cfg = f"""conversation_style:\n - engaging\n - fast-paced\n - enthusiastic\nroles_person1: main summarizer\nroles_person2: questioner/clarifier\ndialogue_structure:\n - Introduction\n - Main Content Summary\n - Conclusion\npodcast_name: Podcastfy\npodcast_tagline: Your Personal Generative AI Podcast\noutput_language: English\ncreativity: 1\nuser_instructions: ""\n\ntext_to_speech:\n default_tts_model: edge\n output_directories:\n transcripts: \"{(out_dir / 'transcripts').as_posix()}\"\n audio: \"{(out_dir / 'audio').as_posix()}\"\n edge:\n default_voices:\n question: \"{voice_q}\"\n answer: \"{voice_a}\"\n audio_format: mp3\n temp_audio_dir: \"{(out_dir / 'tmp').as_posix()}/\"\n ending_message: See You Next Time!\n"""
path = out_dir / "conversation_config.yaml"
path.write_text(cfg, encoding="utf-8")
return path
def parse_args(argv: list[str]) -> argparse.Namespace:
p = argparse.ArgumentParser()
p.add_argument("--url", action="append", dest="urls", default=[], help="URL to include (repeatable)")
p.add_argument("--longform", action="store_true", help="Generate long-form content")
p.add_argument("--out", default=str(SKILL_DIR / "output"), help="Output directory")
return p.parse_args(argv)
def newest_file(path: Path, pattern: str) -> Path | None:
files = list(path.glob(pattern))
if not files:
return None
return max(files, key=lambda p: p.stat().st_mtime)
def mp3_is_valid(mp3_path: Path) -> bool:
# Quick size sanity check: broken files we saw were ~261 bytes.
try:
if mp3_path.stat().st_size < 10_000:
return False
except FileNotFoundError:
return False
# Duration sanity check.
try:
p = subprocess.run(
[
"ffprobe",
"-hide_banner",
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"default=nw=1",
str(mp3_path),
],
text=True,
capture_output=True,
check=False,
)
if p.returncode != 0:
return False
# Expect line like: duration=735.144000
dur_line = next((ln for ln in p.stdout.splitlines() if ln.startswith("duration=")), "")
if not dur_line:
return False
duration = float(dur_line.split("=", 1)[1])
return duration >= 1.0
except Exception:
return False
def edge_tts_from_transcript(transcript_path: Path, out_mp3_path: Path) -> None:
if not EDGE_TTS.exists():
raise SystemExit("edge-tts not found in venv; expected at: " + str(EDGE_TTS))
out_mp3_path.parent.mkdir(parents=True, exist_ok=True)
subprocess.run(
[
str(EDGE_TTS),
"-f",
str(transcript_path),
"--write-media",
str(out_mp3_path),
],
check=True,
)
def main(argv: list[str]) -> int:
args = parse_args(argv)
if not args.urls:
raise SystemExit("Provide at least one --url")
if not os.getenv("GEMINI_API_KEY"):
raise SystemExit("Missing GEMINI_API_KEY environment variable")
ensure_ffmpeg()
ensure_venv()
ensure_deps()
out_dir = Path(args.out).resolve()
conv_cfg = write_conversation_config(out_dir)
# Typer/Click compatibility can break the CLI in some environments.
# Use the Python API instead of `python -m podcastfy.client ...`.
code = r'''
import os
import yaml
from podcastfy.client import generate_podcast
urls = os.environ["_PODCASTFY_URLS"].split("\n")
longform = os.environ.get("_PODCASTFY_LONGFORM","0") == "1"
llm_model = os.environ.get("PODCASTFY_LLM_MODEL", "gemini-1.5-flash")
with open(os.environ["_PODCASTFY_CONV_CFG"], "r", encoding="utf-8") as f:
conv = yaml.safe_load(f)
cfg = None
cfg_path = os.environ.get("_PODCASTFY_CFG")
if cfg_path:
with open(cfg_path, "r", encoding="utf-8") as f:
cfg = yaml.safe_load(f)
out = generate_podcast(
urls=urls,
tts_model="edge",
llm_model_name=llm_model,
api_key_label="GEMINI_API_KEY",
config=cfg,
conversation_config=conv,
longform=longform,
)
print(out)
'''
env = os.environ.copy()
env["_PODCASTFY_URLS"] = "\n".join(args.urls)
env["_PODCASTFY_LONGFORM"] = "1" if args.longform else "0"
env["_PODCASTFY_CONV_CFG"] = str(conv_cfg)
# Optional base config overrides (e.g., website_extractor timeout)
cfg_path = SKILL_DIR / "config.yaml"
if cfg_path.exists():
env["_PODCASTFY_CFG"] = str(cfg_path)
proc = subprocess.run([str(PY), "-c", code], env=env, text=True)
if proc.returncode != 0:
return proc.returncode
# Find newest outputs and validate.
audio_dir = out_dir / "audio"
tx_dir = out_dir / "transcripts"
mp3 = newest_file(audio_dir, "*.mp3")
if not mp3:
raise SystemExit(f"No MP3 produced under: {audio_dir}")
if mp3_is_valid(mp3):
print(str(mp3))
return 0
# Fallback: re-synthesize from newest transcript.
tx = newest_file(tx_dir, "*.txt")
if not tx:
raise SystemExit(f"MP3 appears invalid and no transcript found under: {tx_dir}")
fixed = audio_dir / (mp3.stem + "_fixed.mp3")
edge_tts_from_transcript(tx, fixed)
if not mp3_is_valid(fixed):
raise SystemExit(f"Fallback edge-tts MP3 still invalid: {fixed}")
print(str(fixed))
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))