Merge branch ‘feat/topic-tags-and-chat-recall’: 主题标签体系 + 聊天标签召回 Co-Authored-By: Claude Fable 5 noreply@anthropic.com
Merge branch ‘feat/topic-tags-and-chat-recall’: 主题标签体系 + 聊天标签召回
Co-Authored-By: Claude Fable 5 noreply@anthropic.com
📋 代码审查报告已生成,详见 docs/CODE_REVIEW.md,请在下次开发前优先查阅并解决其中标记的问题。
AI 内容聚合与本地化平台
自动抓取 AI 领域的博客、播客和视频,转录成文字,生成中文 Markdown 文档
serve
http://localhost:8000/
/docs
本地 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 # TWITTER_BEARER_TOKEN=... # 可选,无则跳过 Twitter 源
转录功能依赖远程 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
需配置 TWITTER_BEARER_TOKEN。已配置:Sam Altman · Dario Amodei · Yann LeCun · Andrew Ng · Andrej Karpathy。
TWITTER_BEARER_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/ # 840 个测试用例
两套产物目录有意不同构,互链靠文件名 stem 同名,不靠目录对应:
data/output/markdown/<源>/<date>_<title>_<id10>.md
id10
fetched
transcribed
## 原文转录
## 中文翻译
src/workflow/document_writer.py
<vault>/<subdir>/<类型组>/<源>/<同名 stem>.md
src/workflow/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
/
/sources
/contents
/tasks
/maintenance
技术栈:FastAPI + Jinja2 + Tailwind CDN + HTMX,无独立前端构建,无 Node.js。
serve 启动时同时拉起 APScheduler(scheduler.enabled: false 可整体关闭,间隔都在 config/settings.yaml 的 scheduler: 段):
scheduler.enabled: false
scheduler:
调度器是进程内组件,不是分布式队列。同一数据库和输出目录同一时间只能有一个 serve 实例负责周期作业;如果需要发布新版本,请先停止旧进程,再启动新进程。
periodic-fetch-cycle
process-cycle
summarize-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/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 领域的博客、播客和视频,转录成文字,生成中文 Markdown 文档
功能特性
serve启动后访问http://localhost:8000/看仪表盘/源/内容/任务/维护/docs自带 Swagger UI架构概览
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)
需配置
TWITTER_BEARER_TOKEN。已配置:Sam Altman · Dario Amodei · Yann LeCun · Andrew Ng · Andrej Karpathy。项目结构
输出目录设计
两套产物目录有意不同构,互链靠文件名 stem 同名,不靠目录对应:
data/output/markdown/<源>/<date>_<title>_<id10>.md(id10为内容稳定身份的前 10 位十六进制短码,防止同名不同内容互相覆盖;历史文件仍是旧的无短码命名,两者并存)。单文件随处理阶段”成长”——fetched只有元信息+简介,transcribed回填## 原文转录,手动翻译后回填## 中文翻译。渲染统一在src/workflow/document_writer.py,幂等全量重渲染,按源平铺方便程序回填。<vault>/<subdir>/<类型组>/<源>/<同名 stem>.md,由src/workflow/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/workflow/document_writer.pysrc/workflow/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进程,抢不到锁的那个进程都会在启动时硬失败退出,而不会触碰数据库;副本数量本身仍无法在进程内可靠检测,部署时必须保证不重叠。//sources/contents/tasks/maintenance技术栈:FastAPI + Jinja2 + Tailwind CDN + HTMX,无独立前端构建,无 Node.js。
调度器
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/CONTENT_READING_HANDOFF.md。
开发
工程门禁与配置单一来源的说明见 docs/CI.md。
License
MIT License