Files
contentm_agent/subtitle_ocr_v2.py
T
WorkBuddy df56c2c137 初始化提交:contentm_agent 工作区全量快照
内容分四块:
1、产品规划产出 —— MCN 短视频整合营销工作台的①段五份(1a 需求/1b 竞品/1c 画像/1d 策略/1e 场景)、②段两份(2a 功能/2b 布局)、③段界面(DESIGN.md 契约与令牌表 + mcn-workbench.html 原型 + 实测/会诊/审查三份 + 23 张闸门截图)。
2、开源竞品调研 —— 5 个内容工作台项目的取证原始件与 1b 系列分析文档。
3、参考资料 —— 竞品视频抽帧 1145 张 + 2 个源视频 + 功能点截图。
4、机制侧 —— 协作脚本与状态台账、工作区记忆日志、抽帧/OCR 脚本。

.gitignore 只排运行时日志、脚本备份副本与一次性探针输出,其余按原样入库。
2026-10-08 08:13:02 +08:00

115 lines
4.2 KiB
Python

# -*- coding: utf-8 -*-
"""对关键帧字幕区逐帧 OCR,输出规范要求的时间线表字幕稿。
与 v1 的差异:
- 不再做聚类取代表帧,而是对每一帧都 OCR,避免代表帧模糊/漏句导致整句丢失。
- 字幕区 OCR 用底部 18%,防止双行字幕被截断(检测仍用 15%)。
- 按时间滚动去重(相似度≥0.80 视为同一句,保留最长版本)。
- 输出格式严格为:字幕稿_日期.md,正文用时间线表(时间 | 台词 | 帧文件)。
用法:
python subtitle_ocr_v2.py --frames <帧目录> --video <源视频说明> --out <字幕稿.md>
"""
import argparse, difflib, glob, os, re, sys
import numpy as np
from PIL import Image
def fmt_ts(t):
m = int(t // 60)
s = t - m * 60
return "%02d:%06.3f" % (m, s)
def ts_from_name(name):
m = re.search(r"_(\d+\.\d+)s\.jpg", name)
return float(m.group(1)) if m else 0.0
def crop_subtitle(img, crop_ratio):
"""从底部裁剪指定比例的区域用于 OCR。"""
w, h = img.size
sh = max(1, int(round(h * crop_ratio)))
sy = h - sh
return img.crop((0, sy, w, h))
def has_meaningful_cjk(text):
cjk = len(re.findall(r"[一-鿿]", text))
# 也允许纯英文/数字的短片段,但要求至少有一些可见字符
return cjk >= 2 or len(text.strip()) >= 8
def sim(a, b):
return difflib.SequenceMatcher(None, a, b).ratio()
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--frames", required=True, help="关键帧目录")
ap.add_argument("--video", required=True, help="源视频说明")
ap.add_argument("--out", required=True, help="输出字幕稿路径")
ap.add_argument("--crop", type=float, default=0.18, help="OCR 字幕区裁剪比例")
ap.add_argument("--dup", type=float, default=0.80, help="滚动去重相似度阈值")
args = ap.parse_args()
from rapidocr_onnxruntime import RapidOCR
engine = RapidOCR()
files = sorted(glob.glob(os.path.join(args.frames, "*.jpg")))
print("总帧数:%d" % len(files))
raw = []
for f in files:
sub = crop_subtitle(Image.open(f), args.crop)
# 2x 放大提升 OCR 识别率
sub = sub.resize((sub.width * 2, sub.height * 2), Image.LANCZOS)
res = engine(np.array(sub))
if not res or not res[0]:
continue
txt = " ".join([line[1] for line in res[0] if line[1].strip()]).strip()
txt = re.sub(r"\s+", " ", txt)
if not txt or not has_meaningful_cjk(txt):
continue
ts = ts_from_name(os.path.basename(f))
raw.append((ts, txt, os.path.basename(f)))
print("OCR 有效行:%d" % len(raw))
# 按时间滚动去重:同一句保留最长、最完整的版本,时间取最早
merged = []
for ts, txt, fname in raw:
if not merged:
merged.append([ts, txt, fname])
continue
last_ts, last_txt, last_fname = merged[-1]
if sim(txt, last_txt) >= args.dup:
# 视为同一句,保留内容更长的版本(如果当前更长则替换文本,但保留最早时间)
if len(txt) > len(last_txt):
merged[-1][1] = txt
merged[-1][2] = fname
# 时间保持 earliest
else:
merged.append([ts, txt, fname])
print("去重后:%d" % len(merged))
# 写规范时间线表
date_stamp = os.path.basename(args.out).replace("字幕稿_", "").replace(".md", "")
with open(args.out, "w", encoding="utf-8", newline="\n") as fo:
fo.write("# 字幕稿_%s\n\n" % date_stamp)
fo.write("源视频:%s\n\n" % args.video)
fo.write("> 说明:视频无独立字幕轨,本字幕由关键帧画面字幕区逐帧 OCR 提取并按时间排序;"
"相似度≥%.0f%% 的连续重复句已合并,保留最完整的一句。\n\n" % (args.dup * 100))
fo.write("| 时间 | 台词 | 帧文件 |\n")
fo.write("|------|------|--------|\n")
for ts, txt, fname in merged:
fo.write("| %s | %s | %s |\n" % (fmt_ts(ts), txt, fname))
fo.write("\n共 %d 条字幕。\n" % len(merged))
print("已写出 → %s" % args.out)
if __name__ == "__main__":
main()