Files
ocr_data/README.md
2026-09-28 16:09:31 +08:00

216 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 加速)
## 安装依赖
```bash
pip install -r requirements.txt
```
Linux 上若 `numpy`/`scikit-learn`/`av` 卡在源码编译(PyAV 编译需系统 ffmpeg 开发库),强制用预编译 wheel:
```bash
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple --only-binary=av,numpy,scikit-learn
```
首次运行会从 HuggingFace 下载 Whisper 模型;国内建议先设镜像:
```bash
export HF_ENDPOINT=https://hf-mirror.com
```
## 配置说明
编辑 `config/config.json`:
```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) |
## 使用方法
### 批量处理
```bash
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:
```bash
# 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 逻辑
```bash
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`,应把结果/错误回传给视频主线程统一记录。