- C2: asr_adapter 并入讯飞上传/轮询/解析,复用 extract_audio 分离音频, 新增 doco asr 命令读 C1 热词;真转写 ep001 出 310 句带时间戳 - 补提交 C1 代码(llm.py / term_extract.py)与累积词典 - .gitignore: 挡 doco/programs 下 png/wav/mp4 及中间帧目录、本地 settings Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
8.6 KiB
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>,不返回空串。故空场判定基于文本:
strip()后为空串;- 含 HTML 标签(
<且>); - 不含任何汉字(
\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 大量空转。首选方案:并发 48 路请求(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 设计。