OCR Data - 视频字幕提取与识别系统
项目简介
本项目是一个自动化视频字幕提取系统,通过视频抽帧、OCR 文字识别、音频语音识别等技术,从视频中提取硬字幕并生成 SRT 字幕文件。支持多线程批量处理,并与腾讯云 COS 对象存储集成(上传/下载)。
OCR 主力来自内部 OCR 服务(Tencent Youtu,封装为 tencentcloud_ocr_api);Whisper 仅做辅助校验(识别首尾音频过滤片头片尾台标、与 OCR 文本做拼音匹配),不是字幕文字的主要来源,因此 Whisper 模型大小对最终字幕质量影响有限。
核心功能
- 视频抽帧:用 FFmpeg
fps滤镜单次顺序解码,按固定频率抽帧(默认每秒 3 帧) - 字幕区域检测:通过 OCR 自动定位字幕区域,识别/过滤水印
- 图片拼接优化:将多帧字幕区域裁剪后拼接为大图,大幅减少 OCR 调用次数
- 并发 OCR:getsrt 阶段对拼接图/单独帧并发调用 OCR(
subtitle.ocr_workers控制并发数) - 音频辅助校验:用 Whisper 提取首尾音频文字,与 OCR 结果做拼音匹配
- SRT 字幕生成:合并、去重(处理多种 OCR 差异变体),生成标准 SRT
- 批量处理:
ThreadPoolExecutor多视频并发;日志/计时按线程隔离 - 运行统计:每个视频的起止时间、各阶段耗时、图片处理数量写入
数据.jsonl
项目结构
ocr_data/
├── main.py # 主入口,批量任务调度
├── rerun_srt.py # 复用已有OCR数据重生成SRT(调试getsrt用,不重抽帧)
├── requirements.txt # Python 依赖
├── coscli.exe / coscli-linux-amd64 # 腾讯云 COS CLI 工具
├── config/
│ ├── config.json # 配置文件(OCR密钥、抽帧参数、模型、并发等)
│ └── config_utils.py # 配置管理器(单例)
├── tool/
│ ├── process.py # 单视频处理流程
│ ├── merge.py # 图片拼接、字幕区域检测、音频文字提取
│ ├── getsrt.py # OCR解析、字幕定位、去重、SRT生成(含并发OCR)
│ ├── whisper_manager.py # Whisper 管理器(线程安全单例)
│ ├── Ocr/tencentcloud_ocr.py # OCR API 封装(内部Youtu服务)
│ ├── VideoProcessing/multithread_algo.py # FFmpeg 抽帧
│ └── log/log_manager.py # 统一日志管理器(每视频状态线程本地)
└── video/ # 视频文件存放目录
处理流程
1. 从 COS 下载视频(可选)
↓
2. FFmpeg 抽帧(每秒 3 帧,fps=3.0 精确)
↓
3. OCR 检测字幕区域 & 水印过滤
↓
4. 字幕区域裁剪 & 多帧拼接为大图
↓
5. 对拼接图/单独帧并发调用 OCR
↓
6. Whisper 提取首尾音频文字(辅助校验)
↓
7. 字幕定位 & 文本质量评估 & 相似帧合并去重
↓
8. 生成 SRT 字幕文件
↓
9. 上传结果到 COS & 记录元数据/运行统计
环境要求
- Python 3.10+
- FFmpeg(
ffmpeg+ffprobe,抽帧和音频都依赖,需加入 PATH) - coscli(COS 命令行工具,配置
coscli_path) - CUDA(可选,Whisper GPU 加速)
安装依赖
pip install -r requirements.txt
Linux 上若 numpy/scikit-learn/av 卡在源码编译(PyAV 编译需系统 ffmpeg 开发库),强制用预编译 wheel:
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple --only-binary=av,numpy,scikit-learn
首次运行会从 HuggingFace 下载 Whisper 模型;国内建议先设镜像:
export HF_ENDPOINT=https://hf-mirror.com
配置说明
编辑 config/config.json:
{
"tencentcloud": {
"ocr": { "secret_id": "...", "secret_key": "...", "endpoints": ["http://IP1:PORT", "http://IP2:PORT"] },
"cos": { "secret_id": "...", "secret_key": "...", "region": "ap-chongqing", "bucket": "..." }
},
"audio": { "model_size": "small" },
"splitframe": {
"interval_seconds": 0.33,
"download": true,
"upload_path": "cos://your-bucket/path",
"delete_video": false,
"coscli_path": "./coscli-linux-amd64",
"cosconfig_path": ".cos.yaml"
},
"subtitle": {
"min_confidence": 50,
"similarity_threshold": 0.7,
"ocr_workers": 10
}
}
配置项说明
| 配置项 | 说明 |
|---|---|
tencentcloud.ocr.{secret_id,secret_key} |
OCR 服务凭证 |
tencentcloud.ocr.endpoints |
OCR 服务 IP 列表(多个时按 QPS 加权分发,失败自动摘除重试);不填回退到内置单端点 |
tencentcloud.cos.{secret_id,secret_key,region,bucket} |
COS 凭证与桶信息 |
audio.model_size |
Whisper 模型:tiny/base/small/medium/large-v3,默认 small |
splitframe.interval_seconds |
抽帧间隔,0.33 表示每秒 3 帧 |
splitframe.download |
是否从 COS 下载视频 |
splitframe.delete_video |
处理完是否删除本地视频 |
splitframe.coscli_path / cosconfig_path |
coscli 路径 / COS 配置文件 |
subtitle.min_confidence |
OCR 最低置信度阈值 |
subtitle.similarity_threshold |
文本相似度阈值 |
subtitle.ocr_workers |
getsrt 阶段 OCR 并发线程数(默认 10) |
使用方法
批量处理
python main.py --source-list ./text2 --max-workers 10
命令行参数
| 参数 | 默认值 | 说明 |
|---|---|---|
--source-list |
./text2 |
任务 txt 文件所在目录 |
--max-workers |
自动(按本机CPU核数) | 最大并发数;不填/传0则自动选(若本机IP在 tencentcloud.ocr.endpoints 里,判定为OCR共置机,自动打对折让资源;再按可用磁盘÷每视频预估占用(dispatch.est_disk_gb_per_video,默认2G)收紧一次,两者取小;最后还有个硬上限30,超高核数机器也不会自动选出离谱的并发数) |
--mem-guard-gb |
2.0 |
可用内存低于此值(GB)时暂停启动新视频,防 OOM;设 0 关闭 |
--disk-guard-gb |
10.0 |
本地磁盘可用空间低于此值(GB)时暂停启动新视频,给下载/抽帧/合成图预留空间,防止磁盘写满导致级联失败;设 0 关闭 |
启动时会打印一行机器资源(CPU 核数 / 可用内存 / 可用磁盘 / 负载)与当前并发设置,并对不合理的参数(内存/磁盘阈值过高、并发数过大/过小)给出告警,便于跑前判断。内存保护仅在 Linux 生效(读 /proc/meminfo 的 MemAvailable);磁盘保护跨平台(shutil.disk_usage)。机器无 swap 时内存保护尤其重要——可用内存触底会被暂停而非被 OOM 杀进程。
任务列表格式
在 --source-list 目录下创建 .txt 文件,每行一个完整的 COS 路径(上传目标目录由该路径自动推导,无需 --cos-target):
cos://bucket/0004/data/XXX/XXX_p1/XXX_p1.mp4
cos://bucket/0004/data/XXX/XXX_p2/XXX_p2.mp4
每个 txt 处理完会被删除;主循环持续监控目录直到没有 txt。跨运行续传/去重通过读取 数据.jsonl 的 video_cos_path 实现(不依赖 ./success/)。
压测 OCR 端点 / 多 IP 负载均衡
OCR 服务有多个 IP 时,先压测每个 IP 的实际 QPS,运行时按 QPS 加权把请求分发到不同 IP:
# 1) 在 config.json 的 tencentcloud.ocr.endpoints 填上所有 IP
# 2) 压测各端点 QPS,结果写入 config/ocr_weights.json
python bench_ocr_endpoints.py [样例图片] --requests 60 --concurrency 10
# 3) 之后正常跑 main.py,OCR 请求会按权重自动分发;某端点连续失败会被冷却摘除并换端点重试
不跑压测时,所有端点等权轮询;压测产出的 config/ocr_weights.json 存在则按 QPS 加权(错误率>50% 的端点权重置 0,不参与分发)。
调试 SRT 逻辑
python tools/rerun_srt.py ./split/<video_name>/composite
复用已有 ocr_result.jsonl 重生成 SRT,不重新调用 OCR,用于快速迭代 getsrt.py 的去重/合并/时间轴逻辑。注意它不重新抽帧,无法验证抽帧相关改动。composite 目录现在处理完就随 split_dir 一起删了(不再上传/保留到 ./composite/),所以这条命令只在处理还没走完清理那一步之前有效——比如直接 import tool.process 调用 process_single_video() 单独调试时(这条路径不经过 main.py/run_worker.py 的清理逻辑)。
输出说明
<video> 现在是 tool/process.py::derive_video_name(cos_path) 算出来的(<原文件名>_<完整COS路径MD5前8位>),不是纯文件名——见下面"关键实现要点"里的撞名说明。
| 目录/文件 | 说明 |
|---|---|
./video/<video>.mp4 |
下载的源视频(除非 delete_video) |
./split/<video>/ |
处理中的临时抽帧与 composite。composite(拼接大图、ocr_result.jsonl 等)不上传 COS,处理完这个视频后随 split_dir 一起整体删除本地 |
./srt/<video>.srt |
最终 SRT(上传后移到此处;上传到COS的文件名是 <video>.ocr.srt) |
./info/<video>.json |
处理成功的元数据(这个视频的 数据.jsonl 记录写完后立即删,不等整批跑完) |
./info_error/<video>.json |
失败/过滤的元数据(同上,立即删) |
./log/<video>.json |
每视频日志:program_durations(各阶段耗时)+ custom_stats(图片数量)(同上,立即删) |
数据.jsonl |
每视频记录:状态、srt 路径、时长、起止时间、elapsed_seconds、image_counts、stage_durations |
batch_processor.log |
批量运行日志(Python logging输出)。自动轮转:100M/份,最多留5份 |
app.log |
进程整体stdout/stderr(启动.sh/守护重启.sh用>>重定向到这里,比batch_processor.log多包含print()输出)。不走Python logging,轮转在shell层做:启动.sh每次启动前若超过LOG_MAX_MB(默认200)就先轮转;守护重启.sh的tick(每分钟)用copytruncate方式(cp+原地截断)独立轮转,因为进程可能还在用append方式往这个文件写,直接mv会导致新文件一直是空的 |
过滤规则
以下情况会过滤视频(不生成字幕):
- 未检测到字幕区域
- 空白拼接图超过一半
- 冗余文字过多 / 水印占比过大
- 有效字幕数量过少
- 文本质量评分不达标(数字/特殊字符占比过高)
关键实现要点(勿回退)
- 视频本地/COS标识不能只用文件名:源数据里大量不同父目录复用同样的数字文件名(如
0021/17688/00046.mp4和0021/17690/00046.mp4),如果本地工作目录只按文件名(不含父目录)命名,两个不同视频并发处理时会互相踩踏同一份本地文件(./split/00046同时被两边写/删),引发目录未清空崩溃和连锁失败——实测占某台机器全部错误记录的 100%。现在main.py/tool/process.py都调用derive_video_name(cos_path)(<文件名>_<完整路径MD5前8位>)作为唯一标识,两处必须算出同一个值。副作用:最终 SRT 文件名、composite 的 COS 目录名都会带上这个哈希后缀。 - 抽帧频率:每秒 3 帧时 FFmpeg
fps必须是3.0,不能用1/0.33=3.03(会比真正 3fps 快约 1%,导致帧内容与 SRT 标注随帧号累积漂移,18 分钟处可差约 10 秒)。 - SRT 合并的空串前缀:
generate_srt_from_results的"渐进蹦字"判断要求current_text非空,否则空白间隔后的新字幕会继承空白段的起始时间。 - OCR 变体去重:
_similar合并同一句的不同 OCR 损失版本——支持前缀、子序列、标点归一化(······vs......)、去全部标点后的长公共前缀判定(处理开头稳定、中/尾各自丢失的闪烁)。 - 日志线程隔离:
UnifiedLogger的每视频状态是线程本地的;嵌套子线程(如 OCR 并发池)不要直接调logger.log,应把结果/错误回传给视频主线程统一记录。