本文档详细介绍视频转录 API 的系统架构、核心模块设计和处理流程。
┌─────────────────────────────────────────────────────────────┐
│ 用户请求层 │
├─────────────────────────────────────────────────────────────┤
│ FastAPI → verify_token → audit_logger → task_queue (异步) │
└────────────────────────┬────────────────────────────────────┘
│
┌───────────────┼───────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌──────▼─────┐
│ 下载器 │ │ 转录器 │ │ LLM 处理器 │
│ 工厂 │ │ 双引擎 │ │ 协调器 │
│+URL解析 │ │ │ │ (模块化) │
└────┬────┘ └────┬────┘ └────┬───────┘
│ │ │
┌────▼──────────────▼───────────────▼────┐
│ 智能缓存系统 │
│ (SQLite 元数据 + 文件系统) │
│ + URL 解析层(提前缓存检测) │
└────────────────────────────────────────┘
FastAPI lifespan 为每个应用实例创建并绑定独立的 RuntimeContext,配置预检和模块导入不会启动线程、连接数据库或创建运行目录。CLI 启动只加载并验证配置一次,同一个配置对象同时决定 Uvicorn 监听地址和 lifespan 运行时资源。预检会严格验证生命周期字段、维护保留期的类型与范围,以及可选后端 enabled 的布尔类型;后端凭据仅在对应功能明确启用时必填。请求协程、转录线程池和 LLM 消费线程都会显式绑定所属 context,避免并行应用实例共享隐式全局状态。
关闭时按“停止接收与取消后台 owner → 等待转录 producer → 排空 LLM 队列与 worker → 关闭数据库和日志资源 → 关闭通知客户端”的顺序执行,并为 worker 等待设置有界超时。这样在途任务仍可发送最后的成功或失败通知;若 producer 未能在时限内退出,LLM 消费线程、通知客户端和共享资源会保留到进程结束,避免迟到的 LLM 入队无人消费,或关闭中的任务访问已释放连接。
项目使用工厂模式动态匹配下载器,支持以下平台:
| 平台 | 下载器类 | 特殊能力 |
|---|---|---|
| YouTube | YoutubeDownloader | 原生字幕、远程 API 服务器、yt-dlp 下载、实例级缓存优化 |
| Bilibili | BilibiliDownloader | TikHub API、BBDown 工具支持 |
| 抖音 | DouyinDownloader | TikHub API 获取无水印流 |
| 小红书 | XiaohongshuDownloader | TikHub v3 接口 |
| 小宇宙播客 | XiaoyuzhouDownloader | 网页爬虫解析 |
| Apple Podcast | ApplePodcastDownloader | 网页解析获取音频直链 |
| 通用链接 | GenericDownloader | 直接流式下载、断点续传(SSRF 校验 + 重定向逐跳校验) |
工厂模式实现:
def create_downloader(url):
platform_downloaders = [
DouyinDownloader(), BilibiliDownloader(),
XiaohongshuDownloader(), YoutubeDownloader(),
XiaoyuzhouDownloader()
]
for downloader in platform_downloaders:
if downloader.can_handle(url):
return downloader
return GenericDownloader()实例生命周期:每个转录任务创建独立的下载器实例,任务结束后自动销毁,无内存泄漏和并发问题。YouTube 下载器支持实例级缓存,避免任务内的重复 API 请求。
- WebSocket 实时流式传输
- 音频分段处理:25s 片段 + 2s 重叠
- 自动格式转换(MP3/WAV/M4A 等)
- 默认端口:6006
- 支持分片上传(1MB/片)+ MD5 校验
- 说话人分离(Diarization)
- 队列状态查询
- 默认端口:8767
采用 协调器-处理器-核心组件 三层架构:
src/video_transcript_api/llm/
├── coordinator.py # 统一入口 + 场景路由
├── core/ # 核心基础组件(共享)
│ ├── config.py # LLMConfig 统一配置类
│ ├── llm_client.py # LLM 客户端薄封装(重试/翻译/降级由 llm-compat 处理)
│ ├── key_info_extractor.py # 关键信息提取器
│ ├── speaker_inferencer.py # 说话人推断器
│ ├── quality_validator.py # 质量验证器
│ ├── cache_manager.py # 缓存管理器
│ └── errors.py # 错误分类模块
├── processors/ # 独立的处理器
│ ├── plain_text_processor.py # 无说话人文本处理器
│ ├── speaker_aware_processor.py # 有说话人文本处理器
│ └── summary_processor.py # 总结处理器
├── segmenters/ # 分段器
│ ├── text_segmenter.py # 无说话人文本分段器
│ └── dialog_segmenter.py # 有说话人文本分段器
├── prompts/ # 提示词模板
│ └── schemas/ # JSON Schema 定义
└── llm.py # LLM API 调用(通过 llm-compat SyncLLMClient)
LLMCoordinator.process(content, title, ...)
│
├── 步骤 1: 模型选择(风险降级)
│ └── config.select_models_for_task(has_risk)
│
├── 步骤 2: 校对处理(场景路由)
│ ├── str → PlainTextProcessor.process()
│ └── list → SpeakerAwareProcessor.process()
│
├── 步骤 3: 总结生成(基于校对文本)
│ └── SummaryProcessor.process()
│
└── 步骤 4: 合并结果
└── {calibrated_text, summary_text, key_info, stats}
- 提取关键信息 — 从视频元数据提取人名、术语、品牌(KeyInfoExtractor)
- 说话人推断(仅对话流)— 按说话人采样发言样本 + 首次出场上下文,结合关键信息推断真实姓名,置信度低于阈值时降级为"说话人N"占位符(SpeakerInferencer,详见下方"说话人推断")
- 智能分段 + 分段校对 — 并发处理,质量验证(TextSegmenter / DialogSegmenter)
- 质量验证 — 长度检查 + 可选 LLM 打分(QualityValidator)
SpeakerInferencer(llm/core/speaker_inferencer.py)按说话人采样而非全局前 N 字符截断,确保晚出场的说话人也能拿到足够样本:
- 每个说话人取前
samples_per_speaker条发言(默认 3 条,每条截断 120 字符),总字符数不超过max_chars_per_speaker(默认 400) - 首次出场前额外采集
context_dialogs条他人发言作为上下文(默认 2 条,用于捕捉"XX你好"之类的称呼线索) - LLM 推断结果携带 confidence;低于
confidence_threshold(默认 0.6)的映射不采用推断姓名,而是降级为"说话人N"占位符(N 优先取原始标签数字序号,否则按出场顺序编号),避免把低置信度的猜测当作确定结论展示给用户 - 四个参数均可在
config.jsonc的llm.speaker_inference段配置,见下方"配置参数"
校对(CalibrationStatus)与总结(SummaryStatus)状态定义在 utils/llm_status.py,贯穿 processor → coordinator → llm_ops → cache_manager → 前端这条链路,取代早期"用 None 兼表示跳过和失败"导致的二义性(如"总结处理中..."永久占位符 bug):
| 状态类 | 取值 | 含义 |
|---|---|---|
CalibrationStatus |
full |
全部内容成功由 LLM 校对,没有任何原文兜底 |
partial |
部分内容降级为原文或低质量输出 | |
none |
全部内容降级为原文(LLM 校对完全失败) | |
disabled |
用户通过 processing_options.calibrate=false 主动关闭校对(区别于 none:none 是"尝试了但失败",disabled 是"根本没尝试") |
|
SummaryStatus |
generated |
总结成功生成 |
skipped_short |
原文过短,未触发总结生成(正常路径,非失败) | |
failed |
触发了生成但失败(LLM 异常或输出过短/为空) | |
pending |
总结阶段尚未执行完成 | |
disabled |
用户通过 processing_options.summarize=false 主动关闭总结 |
llm_status.json 表示媒体的当前状态,后续请求可以继续补层并推进它。task_status 则表示一次请求的不可变终态:创建时保存规范化 processing_options 和 submitted_by,artifact/status 原子写入成功后才通过 compare-and-set 写一次 terminal_snapshot;success/failed 终态之后即使 force=True 也不能覆盖。两者不能再被理解为互相镜像。
进程若在 artifact 写入后、任务终态写入前崩溃,artifact 保留,启动恢复只把仍处于 queued/processing/calibrating 的请求标为 failed。repository 的保存、清理和恢复错误必须向维护协调层抛出,由协调层记录并在下一周期重试,不能用返回 0 或 False 伪装成功。终态任务清理没有注入 AuditLogger 时必须拒绝删除,不能绕过审计归档。
处理深度开关(processing_options)与分层缓存复用的完整语义,见 处理深度开关功能文档。
| 组件 | 类名 | 主要职责 |
|---|---|---|
| 统一配置 | LLMConfig |
集中管理所有 LLM 配置,支持风险模型切换 |
| 可靠调用 | LLMClient |
薄封装(重试/翻译/降级由 llm-compat SyncLLMClient 内部处理) |
| 信息辅助 | KeyInfoExtractor |
从视频元数据提取关键信息,作为 Prompt 上下文 |
| 角色还原 | SpeakerInferencer |
将 spk_0 映射为真实姓名 |
| 质量防线 | QualityValidator |
LLM 打分或长度比例验证 |
| 总结生成 | SummaryProcessor |
基于校对文本生成总结,支持单/多说话人模式 |
分段处理:
- 触发阈值:
enable_threshold(默认 5000 字符) - 每段大小:
segment_size(默认 2000 字符) - 并发数:
concurrent_workers(默认 10)
LLM 调用(通过 llm-compat):
- 重试、指数退避、provider 翻译由 llm-compat 内部处理
- 内容审查降级:主模型被拒时自动切换 fallback 模型(通过
content_fallbacks配置) - 可选 Collector 集成:跨项目敏感词积累
质量阈值:
- 整体评分:
overall_score(默认 8.0) - 单项评分:
minimum_single_score(默认 7.0)
说话人推断采样(llm.speaker_inference):
- 每人采样条数:
samples_per_speaker(默认 3) - 每人采样字符上限:
max_chars_per_speaker(默认 400) - 首次出场前上下文条数:
context_dialogs(默认 2) - 置信度阈值:
confidence_threshold(默认 0.6,低于此值降级为"说话人N")
- SQLite 数据库(
data/cache/cache.db):video_cache表:联合主键(platform, media_id, use_speaker_recognition)task_status表:任务状态追踪(queued → processing → success/failed),另有calibration_status/summary_status两列镜像诚实状态(见"诚实状态模型"),终态记录按storage.task_status_retention_days(默认 180 天)周期清理
- 文件系统:存储实际内容(转录文本、LLM 校对/总结、结构化 JSON、
llm_status.json诚实状态文件)
data/cache/
└── {platform}/
└── {YYYY}/
└── {YYYYMM}/
└── {media_id}/
├── transcript_funasr.json
├── transcript_capswriter.txt
├── llm_calibrated.txt
├── llm_summary.txt
├── llm_processed.json
├── llm_status.json # 诚实状态模型:calibration_status/summary_status
├── key_info.json
└── speaker_mapping.json
- 请求带说话人识别时,仅匹配对应缓存
- 请求不带时,优先返回信息更丰富的说话人转录结果
- 完整性验证:文件夹不存在时自动清理数据库记录
- URL 解析优化:下载前提前检查缓存,支持短链接自动解析
data/temp 存放转录的输入:下载的源视频与提取的音频中间件。这些文件转录段一结束即成废物,由 TempFileManager(utils/tempfile_manager.py)统一管理,避免长期堆积撑满磁盘。
每个任务在 data/temp/task_<task_id>/ 下落所有临时文件(含 yt-dlp / BBDown / youtube-api 的中间产物,统一重定向到此目录,不再泄漏到系统 /tmp)。任务目录天然隔离,不同任务即使同名文件也互不影响。
data/temp/
├── task_<id1>/ # 任务1:源视频 + 提取音频 + 下载器中间件
└── task_<id2>/ # 任务2:与任务1隔离
- 治本——终态清理:
process_transcription最外层try/finally中,任务结束(成功 / 失败 / 异常 / 缓存命中)后rmtree该任务目录。临时文件只是转录输入,不依赖 LLM 阶段终态。 - 治标——兜底扫描:进程内的惰性扫描(任务开始时触发,按
temp_retention_hours节流)+ 启动时清扫,清理崩溃 / 强杀残留的孤儿目录。无额外系统组件(无 cron / sidecar)。
扫描删除的条件是「不属于任何活跃任务 且 mtime 超过 temp_retention_hours」双条件。活跃任务(如多小时的直播录像下载)由活跃登记表保护,不会被按 mtime 一刀切误删;优雅关闭时也只清理非活跃任务目录。
相关配置:storage.temp_retention_hours(默认 24 小时)。
- Bearer Token 认证
- 用户启用/禁用控制
- API Key 脱敏显示
- 配置文件:
config/users.json
- 记录 API 端点、请求/响应时间、处理耗时、状态码、用户信息
- LLM token 用量审计(
audit.dbschema v3,llm_usage表):每次LLMClient.call()调用记一行,含task_id/stage(calibration/summary/speaker_inference/validation 等)/model/prompt·completion·total tokens/耗时;provider 未回报用量时仍写入一行并标记usage_missing,避免静默丢弃。json_object 模式的 Self-Correction 重试触发多次真实 API 往返时,桥接槽按顺序累积全部快照并对 token 求和落一行(而非只记最后一次);只要有任一次尝试缺失 usage,该行仍会标记usage_missing=True,已知部分求和仅作为下界参考,不会被误标为完整数据。标题生成(通用下载器场景)已改走LLMClient.call(),计入用量统计 llm_usage全局聚合(按 stage/总计)不区分调用方用户;多用户模式下仅系统所有者视角(单 token 回退模式,或多用户模式下的 legacy fallback token)可见,避免跨租户泄露调用规模/成本信息,见GET /api/audit/stats- 任务审计快照(
audit.dbschema v4,task_audit_snapshots表):终态任务以幂等 upsert 归档标题、平台、提交者、处理选项和状态。升级启动时会用每批最多 500 条的 repair 操作循环完成全部旧终态任务回填,再对外服务;周期维护继续补偿运行期失败。任务清理在cache.db跨进程写锁覆盖下严格执行“复检资格 → 归档 → 清空view_token并标记content_expired→ 删除任务行”;任一步失败都保留任务供后续 cleanup 重试。若删除失败且任务行仍在,会补偿恢复快照 capability;若进程在标记过期后、删除任务行前中断,普通 archive/repair 不会复活已撤销的 capability,下一次 cleanup 会幂等地继续删除任务行。尚未标记内容过期的快照即使暂时没有审计行引用也会保留,避免后续调用审计重新引用该任务时缺失历史;已过期快照仅在最后一条审计引用和对应任务行都消失后清理,不延长正文或转录访问期。 - 公开 capability fail-closed:
view_token查询同时检查 audit-owned 快照;快照一旦标记content_expired,即使进程在删除task_status前中断,公开查看也立即返回未找到,不会在跨库恢复窗口重新开放。 - 查询接口:
GET /api/audit/stats(含llm_usage按 stage 聚合 + 总计)、GET /api/audit/calls、GET /api/audit/history(只查询audit.db的终态日志和快照,不再按请求ATTACH cache.db;状态过滤仅接受success、failed、all,并返回content_expired)
- 基于
wecom-notifier库(v0.3.1+),支持企业微信和飞书双平台 NotificationRouter路由层按配置自动分发到所有启用的渠道- 支持 per-channel webhook:全局配置、用户级配置、per-request 指定
- 渠道 fallback:目标渠道失败时自动退到备用渠道
- 超长文本自动分段、URL 保护模式、频率控制(各渠道独立)
- 通知时机:任务创建、开始处理、缓存命中、转录完成、LLM 完成、ASR 告警
- 远程动态加载敏感词库
- 多策略脱敏:
summary(整体替换)、title(前 6 字符)、general(全移除) - 风险模型自动切换:
risk_calibrate_model、risk_summary_model
三个页面(add_task_by_web、transcript.html、history.html)共享统一站内导航(site-nav),history.html 已适配移动端响应式布局。
访问 GET /add_task_by_web,图形化提交转录任务。
访问 GET /view/{view_token},根据任务状态展示不同页面:
view_token 是不可猜测的公开只读 capability,设计目的就是让处理结果可直接分享;媒体缓存和结果允许跨提交者复用。user_id 只约束提交归属、私有审计历史和用量统计,不把公开内容改造成严格租户资源。重新校对、删除等写操作仍需认证与相应权限。任务清理后 capability 失效,但 audit snapshot 只保留元数据和 content_expired 标记,不延长正文访问期。
| 状态 | 模板 |
|---|---|
processing |
processing.html |
success |
transcript.html(含总结、校对文本、浮动目录、正文"复制内容"按钮) |
failed |
error.html |
file_cleaned |
cleaned.html |
transcript.html 按诚实状态模型渲染:校对区在 calibration_status 为 partial/none 时展示质量警告条,为 disabled 时展示"未启用 AI 校对"提示;总结区按 summary_status 展示四态文案(pending→处理中、skipped_short→文本过短未生成、failed→生成失败、disabled→未启用)。
| 模式 | 地址 | 返回格式 | 适用场景 |
|---|---|---|---|
| Raw | ?raw=calibrated |
纯文本 | 程序抓取、复制到 AI 平台 |
| Page | ?page=calibrated |
HTML 页面(含 meta 标签) | 爬虫抓取、浏览器阅读 |
| File | /export/{token}/{type} |
文件下载 | 离线使用 |
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
url |
string | 是 | 平台链接 |
use_speaker_recognition |
boolean | 否 | 是否启用说话人识别(默认 false) |
wechat_webhook |
string | 否 | 企业微信 webhook 地址(向后兼容) |
notification_config |
object | 否 | 通知配置:{channel: "feishu", webhook: "..."} |
download_url |
string | 否 | 实际下载地址(跳过平台下载器) |
metadata_override |
object | 否 | 元数据覆盖(title/description/author) |
processing_options |
object | 否 | 处理深度开关:{calibrate: bool=true, summarize: bool=true, infer_speaker_names: bool=true},null 等价于全部启用 |
详见 Download URL 与 Metadata Override 功能文档、处理深度开关功能文档。
- 在
src/video_transcript_api/downloaders/创建下载器类 - 继承
BaseDownloader,实现can_handle()、get_video_info()、download_file() - 在
factory.py中注册 - 添加测试用例