目录

Podcast Hunter

📋 代码审查报告已生成,详见 docs/CODE_REVIEW.md,请在下次开发前优先查阅并解决其中标记的问题。

AI 内容聚合与本地化平台

自动抓取 AI 领域的博客、播客和视频,转录成文字,生成中文 Markdown 文档

Python 3.10+ License: MIT


功能特性

功能 说明
多源采集 RSS 博客/播客、YouTube 频道、Twitter/X 账号
字幕优先 YouTube 有字幕直接下载,无字幕才提取音频转录
双语 ASR 中文 → FunASR + 3D-Speaker(说话人分离),英文 → WhisperX large-v3
长音频 自动识别超长音频(>2h),WhisperX 按块处理,FunASR 内置 VAD 无限制
AI 摘要 转录后自动生成中文摘要,导出 Obsidian 知识库(Anthropic 协议)
智能翻译 全文中文翻译(OpenAI 兼容协议:硅基流动/DeepSeek 等),手动触发
远程 ASR GPU 转录运行在远程服务器,本地只需 SSH 隧道
定时调度 APScheduler 三条周期 lane(fetch 采集 / process 转录 / summarize 摘要)+ 每日清理孤儿音频
Web UI serve 启动后访问 http://localhost:8000/ 看仪表盘/源/内容/任务/维护
REST API FastAPI,/docs 自带 Swagger UI

架构概览

本地 Mac(podcast_hunter)
  │
  ├─ 采集  →  RSS / YouTube / Twitter
  ├─ 存储  →  SQLite + 本地文件
  │
  ├─ 转录  →  SSH 隧道 → 远程 ASR 服务(GPU 服务器)
  │                         ├─ 中文  →  FunASR + 3D-Speaker
  │                         └─ 英文  →  WhisperX large-v3
  │
  ├─ 摘要  →  LLM(Anthropic 协议)→ Obsidian 知识库(自动,stage→summarized)
  └─ 翻译  →  LLM(OpenAI 兼容协议)→ 成品 md 回填(手动触发,stage→completed)

ASR 服务地址http://62.234.59.148:8001(需 SSH 隧道,见下方配置)


快速开始

1. 安装依赖

git clone <repo-url>
cd podcast_hunter

# 核心环境(RSS 采集、API、数据库)
uv sync

# 开发/测试
uv sync --extra dev --extra test

# YouTube 下载(本地需要 yt-dlp + ffmpeg)
uv sync --extra media
brew install ffmpeg yt-dlp   # macOS

2. 环境变量

cp .env.example .env   # 没有就手动创建

.env 最少配置:

OPENAI_API_KEY=sk-...                 # 翻译必填(OpenAI 兼容协议)
OPENAI_BASE_URL=https://api.siliconflow.cn/v1
OPENAI_MODEL=deepseek-ai/DeepSeek-V4-Flash
PH_SUMMARY_TOKEN=...                  # 摘要必填(Anthropic 协议)
PH_OBSIDIAN_VAULT=/path/to/vault      # 摘要导出 Obsidian 时必填
# PH_WEB_TOKEN=...                    # 可选,设置后 Web/API 需带 x-api-token
# TWITTER_BEARER_TOKEN=...            # 可选,无则跳过 Twitter 源

3. 开启 ASR SSH 隧道

转录功能依赖远程 GPU 服务器,需要在本地保持隧道运行

ssh -p 6002 -N -f -L 8001:localhost:8001 user@62.234.59.148

验证是否通:

curl http://127.0.0.1:8001/health
# {"status":"ok","workers":1,...}

说明:隧道关闭后转录会跳过(报警告但不崩溃)。采集和字幕下载不依赖隧道。

4. 初始化并运行

# 建数据库表(首次必须,源配置在 fetch 时自动同步)
python main.py init

# 查看所有配置的源
python main.py list

# 抓取所有内容(采集 + 下载字幕/音频 + 转录,首次会同步 source 到 DB)
python main.py fetch

# 只抓取某一个源
python main.py fetch --source "硅谷101"

# 查看状态
python main.py status

# 启动 Web UI + REST API
#   Web UI:     http://localhost:8000/
#   API docs:   http://localhost:8000/docs
python main.py serve

# 清理孤儿音频(转录失败/中断遗留的 MP3)
python main.py cleanup --dry-run    # 预览
python main.py cleanup              # 真清

内容源

当前配置的源分四类,配置文件在 config/sources/

播客(podcasts.yaml)

播客 语言 说明
硅谷101 🇨🇳 中文 硅谷科技与 AI,FunASR 转录
十字路口 Crossing 🇨🇳 中文 AI 与科技深度访谈,FunASR 转录
Training Data 🇺🇸 英文 Sequoia Capital AI 播客
Dwarkesh Podcast 🇺🇸 英文 科技/历史深度访谈
Lex Fridman Podcast 🇺🇸 英文 AI/科技领袖访谈
Acquired 🇺🇸 英文 科技公司商业史
Google DeepMind: The Podcast 🇺🇸 英文 DeepMind 研究
No Priors 🇺🇸 英文 AI 创业与 VC
The TWIML AI Podcast 🇺🇸 英文 ML 从业者访谈

YouTube(youtube.yaml)

3Blue1Brown · Yannic Kilcher · Two Minute Papers · Andrej Karpathy · Lex Fridman

字幕策略:有字幕 → 直接下载(不转录);无字幕 → 提取音频 → 远程 ASR 转录。

博客(blogs.yaml)

OpenAI · Anthropic · Google AI · DeepMind · Hugging Face · AI Alignment Forum · Distill · Lil’Log

Twitter(twitter.yaml)

需配置 TWITTER_BEARER_TOKEN。已配置:Sam Altman · Dario Amodei · Yann LeCun · Andrew Ng · Andrej Karpathy。


项目结构

podcast_hunter/
├── main.py                      # CLI 入口(argparse 分发到 src/cli/)
├── config/
│   ├── settings.yaml            # 系统配置(ASR 引擎、LLM 模型注册表、调度间隔等)
│   └── sources/                 # 内容源配置,按类型分文件
│       ├── blogs.yaml
│       ├── podcasts.yaml        # language: zh/en 决定 ASR 引擎路由
│       ├── youtube.yaml
│       └── twitter.yaml
├── src/
│   ├── cli/                     # CLI 子命令实现(fetch / status / transcribe / translate)
│   ├── collectors/              # 采集器(RSS、youtube/ 子包、Twitter)
│   ├── processors/              # 内容处理器(音频下载、HTML 清洗)
│   ├── transcription/           # 转录:remote_asr.py(推荐)+ local_whisperx.py / whisper_engine.py(备用)
│   ├── llm/                     # LLM 基础设施层:命名模型注册表 + provider 客户端(唯一管 SDK/key/重试/超时)
│   ├── translation/             # 全文翻译业务层(LLMTranslator,OpenAI 兼容协议,消费 src/llm)
│   ├── summarization/           # AI 摘要业务层(LLMSummarizer,Anthropic 协议,消费 src/llm)
│   ├── workflow/                # 工作流编排(功能归属见下表)
│   ├── scheduler/               # APScheduler 封装(scheduler.py)+ 周期作业定义(jobs.py)
│   ├── maintenance/             # 孤儿音频清理(cleanup.py)
│   ├── storage/                 # SQLAlchemy ORM(models_pkg/ 按表拆分)+ Repository(repositories/)+ 文件存储
│   ├── api/                     # FastAPI REST + Web UI
│   │   ├── routes/              # REST 路由 + web.py(Jinja2 模板路由)
│   │   └── templates/           # base + 7 个页面 + partials/
│   └── utils/                   # 工具函数
├── scripts/                     # 运维/验证脚本;一次性迁移归档在 scripts/migrations/
├── data/
│   ├── database/                # SQLite(WAL 模式)
│   └── output/
│       ├── audio/               # 下载的音频(转录成功后删除,遗留由 cleanup 清理)
│       ├── subtitles/           # YouTube 字幕
│       ├── transcripts/         # 转录文本
│       └── markdown/<源>/       # 成品文档(两层目录,单文件随阶段回填成长)
└── tests/                       # 840 个测试用例

输出目录设计

两套产物目录有意不同构,互链靠文件名 stem 同名,不靠目录对应:

  • 成品 md(两层)data/output/markdown/<源>/<date>_<title>_<id10>.mdid10 为内容稳定身份的前 10 位十六进制短码,防止同名不同内容互相覆盖;历史文件仍是旧的无短码命名,两者并存)。单文件随处理阶段”成长”——fetched 只有元信息+简介,transcribed 回填 ## 原文转录,手动翻译后回填 ## 中文翻译。渲染统一在 src/workflow/document_writer.py,幂等全量重渲染,按源平铺方便程序回填。
  • Obsidian 摘要笔记(三层)<vault>/<subdir>/<类型组>/<源>/<同名 stem>.md,由 src/workflow/obsidian_exporter.py 导出,按「类型组/源」分层方便人在 vault 里浏览。

功能归属(功能 ↔ 代码位置)

功能 代码归属
采集编排(多源调度) src/workflow/orchestrator.py,具体采集器在 src/collectors/
单条内容处理流程 src/workflow/pipeline.py(ContentPipeline,含语言路由)
周期消费:转录 lane / 摘要 lane src/workflow/pending_processor.py,作业注册在 src/scheduler/jobs.py
手动翻译 lane src/workflow/translation_runner.py(单 session 事务闭环)
成品 md 渲染回填 src/workflow/document_writer.py
Obsidian 摘要导出 src/workflow/obsidian_exporter.py
YouTube 字幕下载 / RSS↔YouTube 集数匹配 src/workflow/subtitle_downloader.py / src/workflow/youtube_episode_resolver.py
转录策略链(字幕优先 → ASR → 下载兜底) src/workflow/transcript_strategies.py
LLM 调用(模型注册表 / key / 重试 / 超时) src/llm/(registry + providers/)
翻译 / 摘要业务逻辑 src/translation/ / src/summarization/
处理阶段状态机(单一事实来源) src/storage/models_pkg/content.py(STAGE_ORDER)
处理时间线(细粒度过程记录) content_processing_attempts 表,Web 内容详情页可视化
孤儿音频清理 src/maintenance/cleanup.py
Web UI / REST API src/api/routes/web.py / src/api/routes/

ASR 引擎配置

config/settings.yaml 中选择引擎:

transcription:
  asr:
    engine: "remote-asr"          # 推荐:远程统一服务
    remote-asr:
      api_base: "http://127.0.0.1:8001"   # 通过 SSH 隧道访问
      language: null              # null=由播客源的 language 字段决定
      poll_interval: 15           # 轮询间隔(秒),长音频适当调大
      timeout_upload: 600         # 上传超时(秒)

播客源中的 language: zh/en 字段决定路由:

language: zh  →  FunASR + 3D-Speaker(中文转录 + 说话人分离)
language: en  →  WhisperX large-v3(英文转录 + 时间戳对齐)
language: null →  WhisperX 自动检测语言

Web UI

python main.py serve 启动后浏览器打开 http://localhost:8000/

当前运行时只支持一个 API 进程、一个应用副本、一个进程内 APScheduler。不要使用 uvicorn --workers >1WEB_CONCURRENCY>1、多个容器副本,或重叠滚动发布;在 P2 lease-based execution 完成前,这些部署会导致周期任务重复执行,且启动回收逻辑可能把另一个仍在运行进程的任务标记为失败。main.py serve 固定以 workers=1 启动,--workers >1 会在参数解析阶段直接报错拒绝;更底层的强制手段是应用启动(lifespan)时对数据库目录下的 .podcast_hunter.runtime.lock 抢占一把非阻塞的 OS 级 flock——无论是绕过 main.py serve 直接跑 uvicorn --workers N,还是先后启动两个独立的 serve 进程,抢不到锁的那个进程都会在启动时硬失败退出,而不会触碰数据库;副本数量本身仍无法在进程内可靠检测,部署时必须保证不重叠。

页面 路径 主要交互
仪表盘 / 4 张统计卡片 + 最近任务 + 最近内容
内容源 /sources HTMX 一键启停;点进去能改 URL / 抓取间隔 / 优先级
内容 /contents 按源筛选 + 分页 + 详情
任务 /tasks “立即触发抓取” 按钮;有 active 时 5s 轮询;取消/重试
维护 /maintenance 一键 dry-run 预览或真清孤儿音频

技术栈:FastAPI + Jinja2 + Tailwind CDN + HTMX,无独立前端构建,无 Node.js。

调度器

serve 启动时同时拉起 APScheduler(scheduler.enabled: false 可整体关闭,间隔都在 config/settings.yamlscheduler: 段):

调度器是进程内组件,不是分布式队列。同一数据库和输出目录同一时间只能有一个 serve 实例负责周期作业;如果需要发布新版本,请先停止旧进程,再启动新进程。

作业 触发 行为
periodic-fetch-cycle 默认每 3600s 全量启用源采集落库
process-cycle 默认每 600s 消费「已抓取未转录」:字幕 / 远程 ASR / 音频下载(唯一周期消费者
summarize-cycle 默认每 900s,需 summarization.enabled: true 转录后生成 AI 摘要 → 导出 Obsidian → stage→summarized
daily-cleanup 每天 03:00 清理 mtime 超期的孤儿音频

处理阶段状态机:initial → fetched → audio_downloaded → transcribed → summarized →(手动翻译)translated → completedsummarized 是当前产品的常态终点;翻译是成本敏感的可选增值,由 Web「翻译」按钮或 task_type='translate' 手动触发。

里程碑

里程碑 时间 内容
M1 — 核心链路(Phase 1-6) 2026-03 下旬 采集 → 存储 → 转录 → 翻译 → 成品 md 全链路 + CLI
M2 — 重构与批次 1-4 2026-04 ~ 05 代码重构(M1-M6)、调度器、孤儿音频清理、Web UI(Jinja2 + HTMX)
M3 — 全链路一致性整改 2026-06-02 统一状态机 + 成品文档回填 + 翻译闭环
M4 — 架构重构 + 摘要自动化 2026-06-03 LLM 基础设施收口(模型注册表)、TranscriptStrategy 链、AI 摘要 → Obsidian 导出
M5 — 项目体检修复 2026-06-10 四视角审查落地:安全收紧、SQLite 加固、假成功修复、summarized 阶段、文档对齐

待办与留置项统一维护在 docs/ROADMAP.md,历史细节见 docs/archive/。 内容阅读页近期修复记录 + 待排查事项见 docs/CONTENT_READING_HANDOFF.md

开发

# 运行测试
uv run pytest tests/ --ignore=tests/e2e -q

# 真实链路测试(需网络)
PODCAST_HUNTER_RUN_REAL_DOWNLOAD=1 pytest tests/integration/ -v

# 代码检查(main.py / src / tests / scripts 全部纳入,门禁基线为零告警)
# 唯一 lint 工具是 flake8,配置在 .flake8(line-length 120),不是 ruff
uv run flake8 main.py src tests scripts

# 安装 pre-commit 钩子(black + isort + flake8,line-length 统一 120)
uv run pre-commit install
uv run pre-commit run --all-files   # 手动对全仓库跑一遍

# CI 三件套(平台无关脚本,详见 docs/CI.md)
bash scripts/ci/lint.sh   # lint
bash scripts/ci/test.sh   # 单元 + 集成,带覆盖率门禁(--cov-fail-under=75)
bash scripts/ci/e2e.sh    # e2e(跑在 nightly,非每次 PR)

工程门禁与配置单一来源的说明见 docs/CI.md

License

MIT License

关于
35.5 MB
邀请码
    Gitlink(确实开源)
  • 加入我们
  • 官网邮箱:gitlink@ccf.org.cn
  • QQ群
  • QQ群
  • 公众号
  • 公众号

版权所有:中国计算机学会技术支持:开源发展技术委员会
京ICP备13000930号-9 京公网安备 11010802047560号