# Doco 子项目 · P2 完工快照

> 模块定位:《军事科技》中台组成部分。将节目稿件半成品(A稿)、OCR 扒词文档(B稿)、节目纯净声音(ASR)交叉验证融合,产出最接近播出的终版文稿。
> P2 阶段目标:本地部署免费 OCR + 重写字幕扒词流水线,产出高质量 B 稿。
> 状态:**已完工**。本期(ep001 现代防空反导大对决)B稿_v2.txt 定稿,743 行。
> 快照时间:2026-06-15

---

## 一、本阶段为什么存在 / 解决了什么

接手时,旧帧筛流水线把本期七百多条字幕压成了 **308 张关键帧,悄无声息丢了约六成**,且被"308 张 ≈ 300 条字幕,基本一对一"这个**巧合**骗过了验收。

经逐帧决策表(frame_analysis_debug.csv)定位,根因是**用像素启发式去猜字幕变没变**,两个独立 bug:
- **空白过滤误杀**:`max_brightness>=240 AND white_ratio>=0.005` 把字幕淡入/切换的暗帧判成空场(如"我是主持人蓝皓"被剔除)。
- **dHash 距离去重误并**:字幕版式全相同(黑底白字底部居中),仅文字不同,感知哈希分不开"同一条的不同帧"与"两条不同字幕";阈值 5 把不同内容误判为重复(如"欢迎收看《军事科技》"被砍)。

**结论**:像素相似度无法承担"字幕是否改变"的裁判职责。改为 **OCR 优先 + 按文本去重**——把裁判权交给文本(唯一真相层)。

---

## 二、部署:Ollama + DeepSeek-OCR(可复制到离线小机房)

- DeepSeek-OCR 已是 **Ollama 官方库正式模型**,无需第三方 GGUF。`ollama pull deepseek-ocr`。
- 模型 `deepseek-ocr:latest` = `:3b`,**6.7GB**(显存占用约 7.8GB),8K context,Text+Image。
- 要求 Ollama **≥ v0.13.0**;本机实测 0.30.8。
- GPU:制片人主机 RTX 4090D 24GB,`ollama ps` 确认 `100% GPU`。
- 调用:`POST http://localhost:11434/api/generate`,body `{"model":"deepseek-ocr","prompt":"Free OCR.","images":[<base64>],"stream":false,"keep_alive":-1}`,读 `response` 字段。
- **Prompt 用 `Free OCR.`**;实测黑底白字单行字幕识别准、标点干净、无需图像预处理。**禁用** `<|grounding|>...markdown`(会输出 bounding box / markdown 污染纯文本)。

### 部署踩坑结论(每条都是教训,务必保留)
- **`keep_alive:-1`**:不设的话模型闲置 5 分钟被踢出显存,下个请求遇重载空档报 HTTP 503。设 -1 永久常驻。
- **Windows 控制台编码**:脚本顶部 `sys.stdout.reconfigure(encoding="utf-8", errors="replace")`;进度打印**只打数字、不打中文**,否则 GBK 控制台 `UnicodeEncodeError` 崩进程。
- **写文件用 UTF-8**:jsonl 写入显式 `encoding="utf-8"`(`ensure_ascii=False` 可选,转义不影响 `json.loads` 解回)。
- **GPU 利用率低是正常的**:单张小图 OCR,GPU 算一下歇一下,空隙是读图/编码/HTTP/写盘。判断在不在 GPU 看 `ollama ps` 的 PROCESSOR 列和显存占用,**不看利用率百分比**。

### 离线小机房搬运(待执行)
小机房无外网,不能 `ollama pull`。在主机 pull 后,整拷 `C:\Users\<用户名>\.ollama\models`(或经 `OLLAMA_MODELS` 指定的目录)到小机房同路径。

---

## 三、流水线架构:两阶段解耦

**设计原则**:Stage A(贵、慢、易中断)与 Stage B(纯文本、秒级、可反复重跑)完全解耦,中间用 `ocr_raw.jsonl` 缓存衔接。这套设计在本期实战中扛住了 Cline 反复掐进程,**一帧 OCR 没有白跑**。

### Stage A — `stage_a_extract_ocr.py`(抽帧 + OCR)
- ffmpeg 1fps 抽帧 + crop 下方 20%(滤镜 `fps=1,crop=iw:ih*0.2:0:ih*0.8`),输出 `frames_v2/frame_%04d.png`(1-based,`t_sec = NNNN-1`)。
- **每帧都 OCR,绝不做亮度判空、绝不做 dHash/IoU 过滤**(这是硬约束,丢六成的元凶)。
- 结果逐帧追加写 `ocr_raw.jsonl`:`{"idx","t_sec","text"}`,异常帧写 `{..,"text":"","error":...}`。
- **健壮性三件套**:① 启动读已有最大 idx 断点续跑,绝不从头/覆盖;② 单帧 `try/except Exception + continue`,异常绝不冒泡崩主循环;③ 503/超时指数退避重试。

### Stage B — `stage_b_dedup_output.py`(文本去重 + 出稿)
- 只读 `ocr_raw.jsonl`,可反复重跑调阈值。
- **连续段折叠**(裁判在此,基于文本不基于像素):只合并**时间相邻**帧,文本 difflib 相似度 **≥ 0.85** 视为同一条;**只折叠连续段,绝不全局去重**(片头片尾都出现"军事科技"是两条合法记录)。段内**多数投票**取最终文本,**取最早 t_sec** 为时间戳。
- 出稿前清 markdown 残留:`re.sub(r'^[#*\->`\s]+','',text).strip()`(DeepSeek-OCR 偶发 `# ` 行首标题)。
- 输出:`B稿_v2.txt`(`[XmYs] 文本`,格式同旧 B 稿)、`dedup_debug.csv`(逐帧判决)、`blank_filtered.txt`(被判空场的非空文本存档,供审计)。

### `is_blank_ocr(text)` — 空场判定(三选一即空场)
DeepSeek-OCR 在空白/黑场帧会**幻觉**出固定的英文财报表 `<table>...As of December 31...Total return...</table>`,不返回空串。故空场判定基于文本:
1. `strip()` 后为空串;
2. 含 HTML 标签(`<` 且 `>`);
3. 不含任何汉字(`\u4e00-\u9fff`)。
真字幕是纯中文短句,三条都不会误伤。

---

## 四、本期验收数据(ep001)

| 指标 | 数值 |
|---|---|
| 抽帧总数 | 1620 |
| OCR 帧数 / error 帧 | 1620 / **0** |
| 空场数 | 207 |
| 被判空场但非空(去重) | **1 条**(`<table>` HTML 幻觉,正确拦截) |
| **B稿_v2.txt 行数** | **743**(旧 B 稿 742,基本持平) |
| 行首 `#` 残留 | 0 |

**质量结论(对旧 B 稿)**:
- 完整性:从灾难性的 308 → 743,**捞回六成丢失字幕**。两条标志性失踪字幕("欢迎收看《军事科技》""我是主持人蓝皓")均恢复。
- 准确性:归一化后约 589 行与旧稿完全一致;约 130 条差异**绝大多数是新流水线把旧 OCR 错字改对**(盹→吨、肘空→时空、差导→差异、领士→领土、短柝→短板、交镎→交锋、范匡→范围、娈得→变得…),且中文引号更规整。**新 B 稿质量明显高于旧版。**

---

## 五、实战教训(开发期摩擦,非代码缺陷)

- **Cline 反复掐进程**:Cline 作为开发期 agent,天性"检查→行动→再检查",每次"出报告/改加固/重启续跑"都会掐掉前台 OCR 进程,崩点一路从 idx 249→503→756 后移,误以为是 bug。**解法**:让 Cline 彻底松手,改由**制片人自己在独立终端**(`.venv` 激活后 `python stage_a...py`)运行,Cline 碰不到。→ 一次性稳定跑满 1620。
- **进度监控靠文件、不靠终端**:`(Get-Content ocr_raw.jsonl).Count` 数行数判进度;停在某数不动=进程停了,数字在涨=在跑。
- **"503"歧义**:一度把"jsonl 写到 503 行"误读为"HTTP 503",白做一版重试补丁。教训:先看清是行数还是错误码。

> 关键认知:Cline 是脚手架不是运行时。脚本本身是确定性程序,产品化后无人值守运行不会再有被掐现象。本期"自己开终端跑"已提前演练了脱离 Cline 的运行方式。

---

## 六、待办(P2 收尾遗留,进 P3 后并行处理)

- **并发版 Stage A 提速**(制片人已提出):当前严格串行,~30 分钟/期,GPU 大量空转。首选方案:并发 4~8 路请求(`OLLAMA_NUM_PARALLEL`,3B 模型在 24G 卡轻松扛),预计压到十分钟出头。可选叠加"像素完全相同(hash 距离=0)帧复用上帧结果"无损省 OCR。vLLM 原生批量是高上限方案但 Windows 难装、破坏可移植性,非瓶颈不碰。
- **离线小机房模型搬运**(见第二节)。
- **空场清单/审计文件**已落地(`blank_filtered.txt`)。

---

## 七、P3 衔接:三方交叉融合(下一阶段)

**三路输入(本期实测体量与粒度)**:
- **A 稿**(`A稿_..._定稿.docx`,实为 markdown 文本):138 段**散文脚本**,按【导视】【主持人N】【解说N】【专家N】【动画N】【隔断】分段。编辑书面版,**与播出是改写关系**(例:A稿"欢迎来到这一期的《军事科技》节目"→播出"欢迎收看《军事科技》")。**不可逐行 diff**,只宜段落/话题级对齐。
- **B 稿 v2**(`B稿_v2.txt`):743 行**逐条短字幕**,屏幕实况。
- **ASR**(`asr_result_timed.txt`):306 行**句级口语实况**(每行一时间戳起点,常含多句)。

**三方可信维度(P3 融合的核心依据)**:
- A 稿 → 管**结构与书面措辞**(分段、专有名词的规范写法、完整语义)。
- ASR → 管**口语实况**(主持人/专家实际念了什么,含临场改词)。
- B 稿 → 管**屏幕术语拼写**(型号/番号/武器名,如"箭-3""萨德""见证者-136",ASR 常听岔)。

**P3 目标**:以时间戳为轴对齐三方,按各自可信维度投票/择优,产出最接近播出的终版稿。比 P2 复杂,需单独设计。

---

*P2 完工。下一步:P3 设计。*
