原声 · 播客深读
世界太吵,来原声听播客。 每天从 54 档中英文播客里挑出值得记住的判断。要点和金句都带时间戳,点一下就回到 它在原声里被说出的那一秒;金句逐字校验过、数字回原文核对过——查不到出处的, 一律不上站。
线上: https://ourword.ai/podcast/ · RSS · 信源清单
为什么要重做一个
同类站点(如 onepod.site)的做法是:抓 21 个 YouTube 频道的自动字幕,让模型写一篇散记发出来。三个问题:
| 只抓 YouTube 自动字幕 | 本项目 | |
|---|---|---|
| 信源 | 21 个 YouTube 频道,纯音频节目整块缺失 | 54 档,以 Apple Podcasts 官方 RSS 为主干,Acquired / Odd Lots / Invest Like the Best / 中文播客都在 |
| 文稿 | 一层:YouTube 自动字幕,抓不到就发降级篇 | 五层回退,最好的一层是节目自己发布的官方逐字稿(带说话人) |
| 可信度 | 金句无法核对,时间戳没有 | 金句逐字比对逐字稿,对不上当场删;数字回原文核对;时间戳可点击跳原声 |
| 失败时 | 发一篇写着"本机字幕脚本依赖环境异常"的稿子 | 不发。宁可当天零产出 |
第三行是重点。这个仓库里没有"尽力而为"这个选项——pipeline/lib/gate.py 验不过的记录不会变成页面。
内容质量是怎么保证的
一、文稿五层回退(pipeline/lib/transcript.py)
按质量从高到低试,第一个通过密度校验的胜出:
feed— 节目在 RSS 里发布的<podcast:transcript>(VTT / SRT / JSON)。Odd Lots、TBPN、Practical AI、Think Fast、Acquired 有。JSON 那种还自带说话人分离。notes— feed 条目本身就是全文。Substack 系(Latent Space、Interconnects)习惯把整份逐字稿贴进content:encoded,格式是Joon [00:31:12]: …,于是说话人归属和精确时间戳都是免费的。page— show notes 里的 transcript 链接,或节目页。抓下来必须通过标题特征词校验确认是这一集,否则丢弃(否则会抓到节目的归档页)。youtube— yt-dlp 拉自动字幕。会做滚动重复裁剪(YouTube 的字幕是"上一条尾部 + 新内容",直接拼接会把每句说两遍)。asr— 下载音频送 Whisper 兼容接口转写,带 segment 时间戳。长音频用 ffmpeg 切片。
每一层都要过语速校验:英文 70–300 wpm,中文 110–520 字/分。低于下限说明拿到的是 show notes 冒充逐字稿;高于上限说明抓错了文档。两种情况都当作"没有文稿"。
二、编辑质检(pipeline/lib/gate.py)
| 检查 | 不通过时 |
|---|---|
金句在逐字稿里逐字存在(允许 … 省略,保留的每段都要命中) | 删掉该条金句 |
facts 里的数字能在逐字稿里找到 | 删掉该条数字 |
时间戳落在 0 ~ 时长+180s 之间 | 删掉该条 |
| 出现管线元信息(yt-dlp / 本机 / 脚本依赖 / 环境异常 / 转写失败…) | 整篇拒绝 |
| 出现空话套话(值得一听 / 干货满满 / 这期节目讨论了…) | 记入日志 |
| 要点 ≥ 5 条、校验通过的金句 ≥ 2 条、标题是中文 | 整篇拒绝 |
剔除了什么、为什么剔除,都写进 run log(Actions artifact 保留 14 天),页面侧栏也会显示"质检剔除 N 处"。
三、跨源去重
同一集经常同时出现在 RSS 和节目的 YouTube 频道。指纹 = 归一化标题 + 时长按 2 分钟分桶,命中就跳过。(对照站点没做这个,同一集出现过两次。)
事故记录
踩过的坑都在 POSTMORTEM.md,按错误类型分组,每条写清现象、根因、 以及现在靠什么防住。前三类各自重复发生过三到四次(失败却报成功、基础设施抖动被当成 内容缺失、校验规则误杀真内容),改这个项目之前值得先读一遍。
能自动化的都变成了检查:tests/test_guards.py 直接断言那些教训——推送重试循环必须
检查结果、评审失灵必须拦下而选题失灵必须放行、移除信源需连续失败(一次抖动不算)、
评分不许全挤在及格线上、理由与分数必须一致、信源 id 不许因中文名塌成 42/src、
字符串相似度四版失败用例全部保留、判据必须承认"判断与框架"这种价值形态、
每页恰好一个 h1、cron 还开着、本机脚本含提交推送。纯文档防不住重犯。
python3 -m unittest discover -s tests # 87 项
怎么自动更新
有两条路,看你能不能拿到 key。
两条线同时在跑,分工由 IP 决定,互不重复:
| 跑什么 | 频率 | |
|---|---|---|
| 云端 GitHub Actions | 45 档(官方逐字稿 / feed 全文 / 官网文稿页 / RSS 音频转写) | 每天三班 cron |
| 本机 launchd | 16 档(YouTube 与 Substack,机房 IP 抓不到) | 每天 10:30 与 21:30 |
state.json 在 git 里,本机跑前先 git pull,所以不会重复发同一集。
A · 本机定时(已装,不需要任何 key)
用本机已登录的 claude CLI 生成。不产生 API 账单,但会花 Claude 订阅额度,
所以刻意压小:每天 2 篇、模型 sonnet、跳过超过 2 万词的超长集,约 5 万
input tokens/天。
cp scripts/com.ourword.podcast.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.ourword.podcast.plist
每天 10:30 与 21:30 各跑一次(--only-residential,每次上限 6 篇),完整链路并推送。
日志在 .cache/logs/。停掉用 launchctl unload。代价:机器睡着或断网时不跑
(launchd 会在唤醒后补跑)。
转写用本机模型,不需要任何 key:mlx-whisper(Apple Silicon)+ imageio-ffmpeg
自带的静态 ffmpeg。它返回逐句真实时间码,比云端 SenseVoice 那条更准(后者只给整段
文本,得按字数插值)。~/.config/podcast/env 里若放了 Claude 的 LLM_API_KEY(用
scripts/set-local-key.sh 写)就走 API 账单,否则用本机已登录的 claude CLI(花订阅额度,
因此刻意压小篇数)。
B · GitHub Actions 云端日更(只用 Claude)
2026-09-25 起模型只用 Claude,不再用 DeepSeek 或其他 OpenAI 兼容端点。工作流里写死了
LLM_PROVIDER=anthropic 和型号,只要一个 secret:
| secret | 内容 |
|---|---|
ANTHROPIC_API_KEY | Anthropic 控制台的 API key(sk-ant-api…),或 claude setup-token 生成的 token(自动走 Bearer) |
型号分工(写在工作流里,不是 secret):
| 角色 | 型号 | 为什么 |
|---|---|---|
| 选题初筛、分段抽取、深读首稿 | claude-sonnet-5 | 实测便宜模型的首稿 13 集里 11 集持平或更好 |
| 成稿评分 | claude-opus-5 | 不能和首稿同一个模型——自己给自己打分会偏袒 |
| 首稿没过评分时重写 | claude-opus-5 |
没配 ANTHROPIC_API_KEY 时,跑批一开头就报 ::error:: 停下,不会一集集地 401。
配完先跑 python3 pipeline/whoami.py 验证凭证形态,再跑 python3 pipeline/selftest.py
验证全链路。
架构
纯静态站 + GitHub Actions 定时任务。Python 只用标准库(certifi / truststore / yt-dlp 是可选增强)。
pipeline/
run.py 编排:拉取 → 去重 → 打分选题 → 取稿 → 生成 → 质检 → 落盘
build.py data/ → 静态站(首页 / 单集页 / 信源页 / feed.xml / sitemap)
resolve_sources.py 信源清单与健康度;feed 迁移时自动从 Apple 目录重解析
lib/
net.py HTTP:重试、退避、磁盘缓存、三级信任库
feeds.py RSS 2.0(含 podcast 命名空间)与 YouTube Atom,同一个解析器
transcript.py 五层文稿回退 + 语速校验 + 章节抽取
digest.py 结构化深读生成;超长单集走 map-reduce
gate.py 编辑质检
util.py 指纹、时间戳、脱敏
data/
sources.json 54 档信源 + 抓取健康度
episodes/*.json 每集一个文件(唯一文件名,并发跑不会冲突)
state.json 已处理、指纹、失败计数
选题打分(run.py:score):T1 信源 100 分起、T2 55、T3 25;每过一天扣 4 分;自带官方逐字稿 +30;feed 里已有全文 +22;有章节表 +8;短于 15 分钟 −12。每次跑只发前 N 篇。
失败重试:取不到文稿的一集重试 3 次后永久跳过,不会每天重烧预算。
本地跑
python3 -m pip install certifi truststore yt-dlp
python3 pipeline/whoami.py # 凭证探测:两种 header 都试,不打印密钥
python3 pipeline/selftest.py # 全链路自检:信源、模型、转写、取稿
python3 pipeline/resolve_sources.py --check # 体检信源,顺手修好迁移的 feed
python3 pipeline/run.py --dry-run --days 4 # 只到取稿,不调模型
python3 pipeline/run.py --limit 3 # 真跑三篇
python3 pipeline/build.py # 只重建站点
没有配任何 API key 时,lib/llm.py 会自动改用本机已登录的 claude CLI(claude -p)。
这不是零成本。 不产生 API 账单,但会花掉你的 Claude 订阅额度,而且花得很快:
每集要把整份逐字稿(1–3.6 万词)塞进 prompt,超长节目还要按分片各喂一遍。所以
--limit 超过 3 时必须显式加 --spend-subscription,否则拒绝执行。批量建档请配
LLM_API_KEY 走 API。
环境变量 / Secrets
| 变量 | 作用 | 不设会怎样 |
|---|---|---|
LLM_API_KEY | sk-ant-* 走 Anthropic Messages API,其他走 OpenAI 兼容 /chat/completions | 回退到本机 claude CLI,花订阅额度而不是 API 账单 |
LLM_BASE_URL / LLM_MODEL | 覆盖端点与模型。只有设了非 anthropic 的 BASE_URL 才走 OpenAI 兼容路径,否则一律 Anthropic | Anthropic 默认 claude-opus-5 |
LLM_MODEL_TRIAGE / LLM_MODEL_REVIEW | 选题闸门和成稿评分分别用哪个模型。评审最好换一家——同一个模型给自己的作业打分会偏袒 | 都回落到 LLM_MODEL |
REVIEW_MIN / TRIAGE_MIN | 成稿评分与选题闸门的及格线 | 8 / 7 |
LLM_AUTH | bearer = 把 key 当 OAuth token 发(Authorization: Bearer + oauth beta 头);api-key = 发 x-api-key | 按前缀自动判断 |
TRANSCRIBE_API_KEY | 第 5 层音频转写(Whisper 兼容) | 第 5 层关闭,只靠前四层 |
TRANSCRIBE_BASE_URL / TRANSCRIBE_MODEL | 默认 Groq whisper-large-v3-turbo | — |
MAX_NEW / LOOKBACK_DAYS | 每次发布上限 / 回溯天数 | 8 / 10 |
JOBS | 并发处理几集 | 3(claude CLI 后端会自动压到 1——多个 headless 会话并跑会直接退出) |
PODCAST_BASE / PODCAST_SITE | 部署路径与域名 | /podcast · https://ourword.ai |
配完 secrets 跑一次 python3 pipeline/selftest.py,它会分别报"阻塞"和"降级":
阻塞项不修定时任务必失败;降级项能跑,但会损失覆盖率。没有
TRANSCRIBE_API_KEY 时,纯音频节目(全部中文播客、Lenny's、Dwarkesh、
Invest Like the Best)拿不到文稿,按规矩就不会上站——这不是 bug,是那条
"验不过就不发"的规则在起作用。
追赶存量
日更任务只看最近 10 天。要把存档做厚,用 backfill workflow(手动触发)从
自带官方逐字稿的信源往回补——Odd Lots 1260 集、TBPN 649 集、Practical AI 369 集、
Think Fast 322 集、Latent Space 220 集、Acquired 216 集、Dwarkesh 136 集,
这些的文稿都是免费且带时间码的。
gh workflow run backfill.yml --repo woowoeth/podcast \
-f days=180 -f limit=12 -f per_source=4
# 只补某一档
gh workflow run backfill.yml --repo woowoeth/podcast -f only=oddlots -f days=365 -f limit=20
它和日更任务共用一把 concurrency 锁,不会互相踩。每源上限默认 4,避免首页被 单一节目占满。
测试
python3 -m unittest discover -s tests -v
38 个用例,只覆盖"决定什么能发"的纯函数,不碰网络和模型。用例都是开发中真实 踩到的坑:口语数字("twenty fourteen")被当成未验证而删掉、YouTube 滚动字幕 把每句说两遍、章节表冒充逐字稿、同一集算出两个指纹、185 词的一分钟视频被做成 六条要点的深读。
版权
站上内容是原播客的中文深读,版权归各节目所有。每一篇都附原节目链接与时间戳,请去支持原作者。代码 MIT。