Files
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

104 lines
3.7 KiB
Python

# -*- coding: utf-8 -*-
"""关键帧字幕区二值掩码哈希聚类 + 拼字幕长图,供多模态批量认字。
用法:python cluster_subs.py --frames <frames目录> --outdir <长图输出目录> [--thresh 8] [--crop 0.15] [--rows 10]
生成:<outdir>/sheet_001.jpg ... 每张含 rows 句字幕区(原分辨率竖排+时间戳),及 reps.txt 映射。
"""
import argparse, glob, os, re
from PIL import Image, ImageDraw, ImageFont
def crop_subtitle(im, crop):
w, h = im.size
sh = max(1, int(h * crop))
return im.crop((0, h - sh, w, h))
def bin_hash(img, size=24):
"""固定高阈值只抓亮色字幕文字(背景归零)→ 同句字幕跨帧掩码稳定可合并。"""
g = img.convert("L").resize((size + 1, size), Image.LANCZOS)
px = list(g.getdata())
thr = 160 # 字幕文字通常近白(>200),背景暗(<100),固定阈值隔离文字
bits = []
for y in range(size):
row = px[y * (size + 1):(y + 1) * (size + 1)]
for x in range(size):
bits.append(1 if row[x] > thr else 0)
return bits
def hamming(a, b):
return sum(1 for i in range(len(a)) if a[i] != b[i])
def ts_from_name(name):
m = re.search(r"_([0-9]+\.[0-9]+)s\.jpg$", name)
return float(m.group(1)) if m else 0.0
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--frames", required=True)
ap.add_argument("--outdir", required=True)
ap.add_argument("--thresh", type=int, default=25)
ap.add_argument("--crop", type=float, default=0.15)
ap.add_argument("--rows", type=int, default=10, help="每张长图放几句")
args = ap.parse_args()
os.makedirs(args.outdir, exist_ok=True)
files = sorted(glob.glob(os.path.join(args.frames, "*.jpg")))
print("总帧数: %d" % len(files))
# 聚类
clusters = []
for f in files:
sub = crop_subtitle(Image.open(f), args.crop)
hsh = bin_hash(sub)
best, bestd = None, 999
for ci, c in enumerate(clusters):
d = hamming(hsh, c["hash"])
if d < bestd:
bestd, best = d, ci
ts = ts_from_name(os.path.basename(f))
if best is not None and bestd <= args.thresh:
clusters[best]["items"].append((f, ts))
else:
clusters.append({"hash": hsh, "items": [(f, ts)]})
reps = []
for c in clusters:
it = sorted(c["items"], key=lambda x: x[1])
mid = it[len(it) // 2]
reps.append((mid[0], mid[1], len(it)))
reps.sort(key=lambda x: x[1])
print("聚类数(代表帧): %d" % len(reps))
# 拼长图
try:
font = ImageFont.load_default()
except Exception:
font = None
sheets = [reps[i:i + args.rows] for i in range(0, len(reps), args.rows)]
map_lines = []
for si, grp in enumerate(sheets, 1):
crops = [crop_subtitle(Image.open(p), args.crop) for p, _, _ in grp]
wmax = max(c.width for c in crops)
ch = max(c.height for c in crops)
label_h = 28
canvas = Image.new("RGB", (wmax, (ch + label_h) * len(grp)), (255, 255, 255))
d = ImageDraw.Draw(canvas)
yy = 0
for (p, ts, n), c in zip(grp, crops):
d.rectangle([0, yy, wmax, yy + label_h], fill=(230, 230, 230))
d.text((6, yy + 6), "%.3fs (同句帧数=%d)" % (ts, n), fill=(0, 0, 0), font=font)
canvas.paste(c, (0, yy + label_h))
yy += ch + label_h
map_lines.append("%03d\t%.3f" % (si, ts))
canvas.save(os.path.join(args.outdir, "sheet_%03d.jpg" % si))
with open(os.path.join(args.outdir, "reps.txt"), "w", encoding="utf-8") as fo:
fo.write("\n".join(map_lines))
print("长图张数: %d → %s" % (len(sheets), args.outdir))
if __name__ == "__main__":
main()