chore: 忽略一次性 CodeBuddy 交接/规划临时文件(P6c/P7 已完成)
📋 代码审查报告已生成,详见 docs/code_review_final_2026-09-06.md(架构重构 P0–P5 经 Codex 独立复核,无 CRITICAL,文档与守卫缺口已闭环),请在下次开发前优先查阅。 历史各轮与归档去向见审查入口 docs/CODE_REVIEW.md(报告按日期归档,一轮一个文件)。
中文 AI 情报台
自动追踪全球 AI 内容源,把博客、播客、视频和关键人物观点变成中文摘要与可问答知识库,并沉淀到 Obsidian / Markdown
config/taxonomy.yaml
/api/v1/tags
serve
http://localhost:8000/
/docs
内容源按「服务谁、解决什么问题」分成 7 个情报包(config/source_packs.yaml,纯展示层分组,不入库),/sources 与 /contents 页面均可按包筛选:
config/source_packs.yaml
/sources
/contents
本地 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 隧道,见下方配置)
http://62.234.59.148:8001
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
cp .env.example .env # 没有就手动创建
.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 # PH_TWITTER_SYNDICATION_BASE=... # 可选,Twitter 走自建转发器时指向隧道地址(如 http://127.0.0.1:8443) # PH_SYNDICATION_TOKEN=... # 可选,转发器 Bearer token(见 docs/TWITTER_SYNDICATION.md)
转录功能依赖远程 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,...}
说明:隧道关闭后转录会跳过(报警告但不崩溃)。采集和字幕下载不依赖隧道。
# 建数据库表(首次必须,源配置在 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/:
config/sources/
3Blue1Brown · Yannic Kilcher · Two Minute Papers · Andrej Karpathy · Lex Fridman
字幕策略:有字幕 → 直接下载(不转录);无字幕 → 提取音频 → 远程 ASR 转录。
OpenAI · Anthropic · Google AI · DeepMind · Hugging Face · AI Alignment Forum · Distill · Lil’Log
免费 syndication 通道(无需 API token):抓取 X 网页 widget 用的精选历史推文窗口,约 100 条/源。 生产经 VPS 转发器 + SSH 隧道访问(配置 PH_TWITTER_SYNDICATION_BASE + PH_SYNDICATION_TOKEN,见 docs/TWITTER_SYNDICATION.md)。⚠️ 窗口由 X 服务器端精选,非时间倒序、 不保证最新(活跃账号窗口滞后可达 1-2 年),定位是「AI 大佬高互动推文精选库」而非实时时间线。 已配置:Sam Altman · Dario Amodei · Yann LeCun · Andrew Ng · Andrej Karpathy · Elon Musk。
PH_TWITTER_SYNDICATION_BASE
PH_SYNDICATION_TOKEN
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/ # 923 个测试用例
两套产物目录有意不同构,互链靠文件名 stem 同名,不靠目录对应:
data/output/markdown/<源>/<date>_<title>_<id10>.md
id10
fetched
transcribed
## 原文转录
## 中文翻译
src/documents/document_writer.py
<vault>/<subdir>/<类型组>/<源>/<同名 stem>.md
src/documents/obsidian_exporter.py
src/workflow/orchestrator.py
src/collectors/
src/workflow/pipeline.py
src/workflow/pending_processor.py
src/scheduler/jobs.py
src/workflow/translation_runner.py
src/workflow/subtitle_downloader.py
src/workflow/youtube_episode_resolver.py
src/workflow/transcript_strategies.py
src/llm/
src/translation/
src/summarization/
src/storage/models_pkg/content.py
content_processing_attempts
src/maintenance/cleanup.py
src/api/routes/web.py
src/api/routes/
在 config/settings.yaml 中选择引擎:
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/en
language: zh → FunASR + 3D-Speaker(中文转录 + 说话人分离) language: en → WhisperX large-v3(英文转录 + 时间戳对齐) language: null → WhisperX 自动检测语言
python main.py serve 启动后浏览器打开 http://localhost:8000/:
python main.py serve
当前运行时只支持一个 API 进程、一个应用副本、一个进程内 APScheduler。不要使用 uvicorn --workers >1、WEB_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 进程,抢不到锁的那个进程都会在启动时硬失败退出,而不会触碰数据库;副本数量本身仍无法在进程内可靠检测,部署时必须保证不重叠。
uvicorn --workers >1
WEB_CONCURRENCY>1
main.py serve
workers=1
--workers >1
lifespan
.podcast_hunter.runtime.lock
flock
uvicorn --workers N
/
/dashboard
/tasks
/maintenance
技术栈:FastAPI + Jinja2 + Tailwind CDN + HTMX,无独立前端构建,无 Node.js。
摘要生成之后,系统会再调一次模型,按受控词表给每篇内容打 3-5 个主题标签,落在 Content.tags。 用受控词表(而不是让模型自由发挥)是为了避免「大模型 / LLM / 大语言模型」各算一个标签,把标签云打成上千个长尾。
Content.tags
看哪里:/contents 页面的主题标签筛选(?topics= 可多选、AND 组合;与「源标签」?tags= 是两回事,后者筛的是源配置上的标签);GET /api/v1/tags 返回用过的标签及次数;Obsidian 笔记的 frontmatter 里也会带上。
?topics=
?tags=
GET /api/v1/tags
怎么开:config/settings.yaml 的 tagging: 段(默认已开启,词表随仓库提供)。
tagging:
tagging.enabled
main.py
tagging.taxonomy_path
tagging.model_ref
tag-default
models:
OPENAI_*
TAG_API_KEY
TAG_BASE_URL
TAG_MODEL
tagging.max_tags
min_tags
tagging.max_new_terms
tagging.batch_size
concurrency
daily_max_calls
daily_max_tokens
什么时候跑:summarize-cycle 内联抽取;同一轮末尾还会扫一遍「有摘要没标签」的漏网补上,无需人工介入。
summarize-cycle
改词表:config/taxonomy.yaml 直接编辑即可(groups 分组给人看,aliases 是别名 → 规范词)。想从库里的存量摘要重新归纳一版草稿,跑 uv run python scripts/build_taxonomy.py——它只写 config/taxonomy.draft.yaml,人工过目后再另存为正式词表(详见 用户手册 §9)。
groups
aliases
uv run python scripts/build_taxonomy.py
config/taxonomy.draft.yaml
Web UI 每个页面右下角有一个聊天入口,随 serve 一起提供,无需额外配置开关;它复用摘要用的那个模型(models: 段的 summarize-default,即 OPENAI_API_KEY / OPENAI_BASE_URL / OPENAI_MODEL 那套凭证),没配好这套凭证时聊天会返回错误提示。
summarize-default
OPENAI_API_KEY
OPENAI_BASE_URL
OPENAI_MODEL
两种问法:
回答是流式输出(SSE)。答案里的 [#7231] 是内容 ID 引用,可点击跳转;追问时带 [#7231] 可以让模型深入讲这一条。
[#7231]
serve 启动时同时拉起 APScheduler(scheduler.enabled: false 可整体关闭,间隔都在 config/settings.yaml 的 scheduler: 段):
scheduler.enabled: false
scheduler:
调度器是进程内组件,不是分布式队列。同一数据库和输出目录同一时间只能有一个 serve 实例负责周期作业;如果需要发布新版本,请先停止旧进程,再启动新进程。
periodic-fetch-cycle
process-cycle
summarization.enabled: true
summarized
daily-cleanup
处理阶段状态机:initial → fetched → audio_downloaded → transcribed → summarized →(手动翻译)translated → completed。summarized 是当前产品的常态终点;翻译是成本敏感的可选增值,由 Web「翻译」按钮或 task_type='translate' 手动触发。
initial → fetched → audio_downloaded → transcribed → summarized →(手动翻译)translated → completed
task_type='translate'
待办与留置项统一维护在 docs/ROADMAP.md,历史细节见 docs/archive/。 内容阅读页近期修复记录 + 待排查事项见 docs/archive/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。
MIT License
版权所有:中国计算机学会技术支持:开源发展技术委员会 京ICP备13000930号-9 京公网安备 11010802047560号
Podcast Hunter
中文 AI 情报台
自动追踪全球 AI 内容源,把博客、播客、视频和关键人物观点变成中文摘要与可问答知识库,并沉淀到 Obsidian / Markdown
功能特性
config/taxonomy.yaml)自动打 3-5 个主题标签,用于内容筛选、/api/v1/tags与 Obsidian frontmatterserve启动后访问http://localhost:8000/,一级导航为 今日 / 内容库 / 简报 / 来源 / 仪表盘(仅 admin)/docs自带 Swagger UI它为谁而做
情报包
内容源按「服务谁、解决什么问题」分成 7 个情报包(
config/source_packs.yaml,纯展示层分组,不入库),/sources与/contents页面均可按包筛选:架构概览
ASR 服务地址:
http://62.234.59.148:8001(需 SSH 隧道,见下方配置)快速开始
1. 安装依赖
2. 环境变量
.env最少配置:3. 开启 ASR SSH 隧道
转录功能依赖远程 GPU 服务器,需要在本地保持隧道运行:
验证是否通:
4. 初始化并运行
内容源
当前配置的源分四类,配置文件在
config/sources/:播客(podcasts.yaml)
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)
免费 syndication 通道(无需 API token):抓取 X 网页 widget 用的精选历史推文窗口,约 100 条/源。 生产经 VPS 转发器 + SSH 隧道访问(配置
PH_TWITTER_SYNDICATION_BASE+PH_SYNDICATION_TOKEN,见 docs/TWITTER_SYNDICATION.md)。⚠️ 窗口由 X 服务器端精选,非时间倒序、 不保证最新(活跃账号窗口滞后可达 1-2 年),定位是「AI 大佬高互动推文精选库」而非实时时间线。 已配置:Sam Altman · Dario Amodei · Yann LeCun · Andrew Ng · Andrej Karpathy · Elon Musk。项目结构
输出目录设计
两套产物目录有意不同构,互链靠文件名 stem 同名,不靠目录对应:
data/output/markdown/<源>/<date>_<title>_<id10>.md(id10为内容稳定身份的前 10 位十六进制短码,防止同名不同内容互相覆盖;历史文件仍是旧的无短码命名,两者并存)。单文件随处理阶段”成长”——fetched只有元信息+简介,transcribed回填## 原文转录,手动翻译后回填## 中文翻译。渲染统一在src/documents/document_writer.py,幂等全量重渲染,按源平铺方便程序回填。<vault>/<subdir>/<类型组>/<源>/<同名 stem>.md,由src/documents/obsidian_exporter.py导出,按「类型组/源」分层方便人在 vault 里浏览。功能归属(功能 ↔ 代码位置)
src/workflow/orchestrator.py,具体采集器在src/collectors/src/workflow/pipeline.py(ContentPipeline,含语言路由)src/workflow/pending_processor.py,作业注册在src/scheduler/jobs.pysrc/workflow/translation_runner.py(单 session 事务闭环)src/documents/document_writer.pysrc/documents/obsidian_exporter.pysrc/workflow/subtitle_downloader.py/src/workflow/youtube_episode_resolver.pysrc/workflow/transcript_strategies.pysrc/llm/(registry + providers/)src/translation//src/summarization/src/storage/models_pkg/content.py(STAGE_ORDER)content_processing_attempts表,Web 内容详情页可视化src/maintenance/cleanup.pysrc/api/routes/web.py/src/api/routes/ASR 引擎配置
在
config/settings.yaml中选择引擎:播客源中的
language: zh/en字段决定路由:Web UI
python main.py serve启动后浏览器打开http://localhost:8000/:当前运行时只支持一个 API 进程、一个应用副本、一个进程内 APScheduler。不要使用
uvicorn --workers >1、WEB_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进程,抢不到锁的那个进程都会在启动时硬失败退出,而不会触碰数据库;副本数量本身仍无法在进程内可靠检测,部署时必须保证不重叠。//contents/sources/dashboard/tasks/maintenance技术栈:FastAPI + Jinja2 + Tailwind CDN + HTMX,无独立前端构建,无 Node.js。
主题标签
摘要生成之后,系统会再调一次模型,按受控词表给每篇内容打 3-5 个主题标签,落在
Content.tags。 用受控词表(而不是让模型自由发挥)是为了避免「大模型 / LLM / 大语言模型」各算一个标签,把标签云打成上千个长尾。看哪里:
/contents页面的主题标签筛选(?topics=可多选、AND 组合;与「源标签」?tags=是两回事,后者筛的是源配置上的标签);GET /api/v1/tags返回用过的标签及次数;Obsidian 笔记的 frontmatter 里也会带上。怎么开:
config/settings.yaml的tagging:段(默认已开启,词表随仓库提供)。tagging.enabledmain.py启动阶段就直接报错退出,不会静默跳过tagging.taxonomy_pathconfig/taxonomy.yamltagging.model_reftag-default(见models:段,走自建网关,复用OPENAI_*;可用TAG_API_KEY/TAG_BASE_URL/TAG_MODEL单独指定)tagging.max_tags/min_tagstagging.max_new_termstagging.batch_size/concurrency/daily_max_calls/daily_max_tokens什么时候跑:
summarize-cycle内联抽取;同一轮末尾还会扫一遍「有摘要没标签」的漏网补上,无需人工介入。改词表:
config/taxonomy.yaml直接编辑即可(groups分组给人看,aliases是别名 → 规范词)。想从库里的存量摘要重新归纳一版草稿,跑uv run python scripts/build_taxonomy.py——它只写config/taxonomy.draft.yaml,人工过目后再另存为正式词表(详见 用户手册 §9)。内容问答(聊天)
Web UI 每个页面右下角有一个聊天入口,随
serve一起提供,无需额外配置开关;它复用摘要用的那个模型(models:段的summarize-default,即OPENAI_API_KEY/OPENAI_BASE_URL/OPENAI_MODEL那套凭证),没配好这套凭证时聊天会返回错误提示。两种问法:
回答是流式输出(SSE)。答案里的
[#7231]是内容 ID 引用,可点击跳转;追问时带[#7231]可以让模型深入讲这一条。调度器
serve启动时同时拉起 APScheduler(scheduler.enabled: false可整体关闭,间隔都在config/settings.yaml的scheduler:段):调度器是进程内组件,不是分布式队列。同一数据库和输出目录同一时间只能有一个
serve实例负责周期作业;如果需要发布新版本,请先停止旧进程,再启动新进程。periodic-fetch-cycleprocess-cyclesummarize-cyclesummarization.enabled: truesummarizeddaily-cleanup处理阶段状态机:
initial → fetched → audio_downloaded → transcribed → summarized →(手动翻译)translated → completed。summarized是当前产品的常态终点;翻译是成本敏感的可选增值,由 Web「翻译」按钮或task_type='translate'手动触发。里程碑
summarized阶段、文档对齐待办与留置项统一维护在 docs/ROADMAP.md,历史细节见 docs/archive/。 内容阅读页近期修复记录 + 待排查事项见 docs/archive/CONTENT_READING_HANDOFF.md。
开发
工程门禁与配置单一来源的说明见 docs/CI.md。
License
MIT License