如何提取视频中的文案:口播语音识别加画面花字 OCR,两路结果合并去重

我在一家做家居用品的公司负责内容中台。我们的短视频团队两年里拍了五百多条产品视频,现在要建一个文案素材库:运营写新脚本前,能搜到以前写过的卖点和话术。问题是,大部分视频当初没有留下文字稿,文案只存在于视频里,而且分在两个地方:一部分是主播口播的话,一部分是画面上弹出的花字,比如「加厚三层」「承重 200 斤」这类卖点标签。只做语音识别,会漏掉花字;只做画面识别,又会漏掉口播。如何提取视频中的文案才完整?我的做法是两路一起跑:一路用 faster-whisper 识别口播,一路抽帧用 EasyOCR 识别画面文字,再按时间合并、去掉两路重复的内容,最后输出一份按时间排好的文案。下面把每一步的代码写出来。最后也说说不写代码的选择,运营同事临时要一条视频的文案时,我一般让她们用提词匠。

说明:这些都是公司自己拍摄的视频。别人的视频文案受版权保护,提取后直接搬用属于侵权。

一、文案藏在哪几个地方

整理之前,我先看了几十条视频,文案大致分三类来源:

  • 口播:主播对着镜头讲的话,占文案的大头。
  • 画面花字:后期加上的标题、卖点标签、价格提示,很多是口播里没有说的。
  • 发布时写的简介:这部分在作品页面上,不在视频文件里,我们另外从后台导出表格。

本文只处理视频文件本身包含的前两类。两类的特点很不一样:口播连续、句子长,但会有口头禅和识别错字;花字简短、准确,但会在好几帧里重复出现,同一句话识别出十几次。所以合并时,花字要先去重,再和口播对齐。

二、口播:faster-whisper 识别成带时间的句子

faster-whisper 是 OpenAI Whisper 模型的一个高效实现,在普通 CPU 上也能跑,可以直接读视频文件。它返回的每一段都带开始和结束时间,后面合并要用:

from faster_whisper import WhisperModel   # pip install faster-whisper

model = WhisperModel("small", device="cpu", compute_type="int8")


def speech_segments(video: str) -> list[dict]:
    segments, _ = model.transcribe(
        video, language="zh", vad_filter=True,
        initial_prompt="以下是家居用品介绍视频的口播,使用简体中文和标点。",
    )
    return [{"start": s.start, "end": s.end, "text": s.text.strip(), "src": "口播"}
            for s in segments if s.text.strip()]

vad_filter=True 会跳过没有人说话的段落,减少静音处的胡乱输出;initial_prompt 里写一句简体中文,能减少输出繁体字的情况,也可以放进常用的产品名,提高专有名词的识别率。注意 segments 是一个生成器,只能遍历一次,所以这里直接转成列表。

三、花字:每秒抽一帧,EasyOCR 识别

画面文字用开源的 EasyOCR 识别,它支持简体中文。每秒抽一帧就够了,花字一般会停留一两秒以上。识别结果的每一项依次是文字框、文字和置信度,我只保留置信度够高、长度至少两个字的结果:

import cv2
import easyocr   # pip install easyocr

reader = easyocr.Reader(["ch_sim", "en"])


def screen_texts(video: str, every_sec: float = 1.0, min_conf: float = 0.5) -> list[dict]:
    cap = cv2.VideoCapture(video)
    fps = cap.get(cv2.CAP_PROP_FPS) or 25
    step, idx, results = int(fps * every_sec), 0, []
    while True:
        ok, frame = cap.read()
        if not ok:
            break
        if idx % step == 0:
            t = idx / fps
            for _, text, conf in reader.readtext(frame):
                text = text.strip()
                if conf >= min_conf and len(text) >= 2:
                    results.append({"start": t, "end": t + every_sec, "text": text, "src": "花字"})
        idx += 1
    cap.release()
    return results

画面里除了花字,还可能有产品包装上的字、背景里的招牌,这些也会被识别出来。我们的处理方式是后面去重时按出现频率过滤:产品包装上的字几乎每一帧都有,出现时间占整条视频大半的,判定为背景文字丢掉。

四、花字去重:相同的字合并成一条

同一句花字会在连续好几帧里被识别出来,而且每次识别的结果可能有一两个字的差别。我用 Python 自带的 difflib 计算两段文字的相似度,相似度高、时间又相邻的合并成一条,起止时间取并集:

from difflib import SequenceMatcher


def similar(a: str, b: str) -> float:
    return SequenceMatcher(None, a, b).ratio()


def merge_screen(items: list[dict], gap: float = 2.0, sim: float = 0.8) -> list[dict]:
    merged: list[dict] = []
    for it in sorted(items, key=lambda x: x["start"]):
        for m in merged[-5:]:                      # 只和最近几条比,速度快
            if similar(m["text"], it["text"]) >= sim and it["start"] - m["end"] <= gap:
                m["end"] = max(m["end"], it["end"])
                if len(it["text"]) > len(m["text"]):
                    m["text"] = it["text"]         # 保留识别得更完整的版本
                break
        else:
            merged.append(dict(it))
    return merged


def drop_background(items: list[dict], duration: float, ratio: float = 0.6) -> list[dict]:
    return [it for it in items if (it["end"] - it["start"]) < duration * ratio]

合并时保留更长的那个版本,因为识别不全的情况比多识别出字更常见。drop_background 过滤掉在画面里停留超过六成时长的文字,这些基本都是包装和背景上的字。

五、两路合并:去掉花字里口播已经说过的

花字和口播经常是同一句话:主播说「这个收纳箱加厚了三层」,画面上同时弹出「加厚三层」。如果两路都保留,文案里就会重复。我的规则是:一条花字如果在它出现前后几秒内,被口播的句子基本包含了,就不再单独保留;剩下的花字作为口播没提到的补充卖点,按时间插进文案:

def covered_by_speech(text: str, speech: list[dict], t: float, window: float = 3.0) -> bool:
    nearby = "".join(s["text"] for s in speech if s["start"] - window <= t <= s["end"] + window)
    hit = sum(1 for ch in text if ch in nearby)
    return hit / max(len(text), 1) >= 0.8          # 八成以上的字都在口播里出现过


def extract_copy(video: str, duration: float) -> list[dict]:
    speech = speech_segments(video)
    screen = drop_background(merge_screen(screen_texts(video)), duration)
    extra = [s for s in screen if not covered_by_speech(s["text"], speech, s["start"])]
    return sorted(speech + extra, key=lambda x: x["start"])


for item in extract_copy("videos/p0312.mp4", duration=58.0):
    print(f"[{item['start']:5.1f}s][{item['src']}] {item['text']}")

按字统计重合度是个粗糙但好用的办法,中文花字通常就是口播的关键词缩写,八成的字出现在附近的口播里,基本可以判定是同一个意思。输出时标注每一条来自口播还是花字,运营看文案时能分清哪些是说的、哪些是打在屏幕上的。五百多条视频批量跑完,导入素材库后,运营搜「加厚」「承重」就能翻出以前所有相关的话术。

六、不想写代码:运营临时要一两条

这套流程是给素材库批量建档用的。运营同事平时更常见的需求是临时要一两条视频的文案,比如参考上个月某条爆款的口播。让她们装模型、跑脚本不现实,我推荐的是下面几个,运营用得多的是提词匠:

  • 提词匠:做视频、音频转文字的小程序,不用安装。自己账号发过的短视频,可以在首页顶部的「短视频转文字」里粘贴作品链接提取文案;手机或电脑里的本地视频,在「常用工具」里点「视频转文字」上传。转写完在结果页改错字,再导出 TXT、Word 或 SRT 字幕。它识别的是视频里的声音,画面上的花字需要自己补上。限制是需要联网,一次处理一个视频,按识别时长计费,以页面显示为准。
  • 剪映电脑版:视频导入时间线后用识别字幕生成字幕条,适合要顺便剪辑、做成带字幕成片的情况;部分功能需要会员,以软件内提示为准。
  • 通义听悟:适合一小时以上的长视频,比如直播回放,上传后能区分发言人、拆出章节和要点。

七、几种做法怎么选

  • 批量建文案素材库、口播和花字都要:faster-whisper 加 EasyOCR 两路识别,合并去重。
  • 视频基本只有口播、没有花字:只跑语音识别就够了。
  • 运营临时要一两条视频的口播文案:提词匠粘贴作品链接或上传本地视频,改完错字导出。
  • 要顺便剪成带字幕的视频:剪映电脑版。
  • 直播回放这类长视频:通义听悟。

八、常见问题

问:自己账号发过的短视频,怎么快速提取口播文案?

答:用提词匠。在首页顶部的「短视频转文字」里粘贴作品链接,等转写完成,在结果页把错字改掉,再导出 TXT 或 Word。

问:视频在手机相册里,没有作品链接,能提取吗?

答:能。在提词匠的「常用工具」里点「视频转文字」,选择手机里的视频上传,转写完同样可以在结果页修改后导出。

问:提取出来的文案,能直接当字幕用吗?

答:能。在提词匠的结果页选择导出 SRT 字幕,文件带时间轴,可以直接导入剪辑软件当字幕轨,导出前先核对一遍产品名和数字。

问:EasyOCR 识别花字时,把产品包装上的字也识别进来了?

答:按第四节用出现时长过滤,长时间停留在画面里的大多是背景和包装;还可以只识别画面中花字常出现的区域,比如上半部分或中间一条,减少干扰。

问:同一句花字被识别成好几个版本?

答:每帧的识别结果可能差一两个字,用 SequenceMatcher 计算相似度合并即可;相似度阈值设得太高会合并不上,设得太低会把不同的花字合在一起,0.8 左右比较合适。

问:faster-whisper 把产品名识别成了同音字?

答:在 initial_prompt 里写上常用的产品名和品牌名,能明显改善;剩下的在合并之后统一做一次替换。

问:整个流程跑一条视频要多久?

答:主要时间花在语音识别和 OCR 上,和视频时长、电脑配置有关。批量处理时可以把抽帧间隔调大、OCR 只识别部分区域,或者在有显卡的机器上跑。

我的做法

如何提取视频中的文案,我现在的做法是:素材库批量建档时,口播交给 faster-whisper,画面花字每秒抽一帧交给 EasyOCR,花字先合并去重、过滤掉背景文字,再去掉口播已经说过的,按时间合成一份标注来源的文案;运营临时要一两条视频的口播,就用提词匠粘贴链接或上传视频,改完错字导出。素材库上线后,新人写脚本前先搜一遍,同样的卖点再也不用从头想了。

0
0
0
0
评论
未登录
暂无评论