上线我的 2.0

上线我的 2.0

马图图

岁月变迁何必不悔,尘世喧嚣怎能无愧。

16 文章数
1 评论数

从零搭建智能字幕生成系统:ASR识别+文案修正+视频嵌入全流程实战

Matuto
2026-01-03 / 0 评论 / 14 阅读 / 0 点赞

从零搭建智能字幕生成系统:ASR识别+文案修正+视频嵌入全流程实战

做短视频剪辑的朋友应该都遇到过这个问题:视频里的人说话,ASR识别出来的字幕总有些地方不对。错别字、漏字、多字,改起来很烦。这篇文章分享我最近做的一个项目,用标准文案自动修正ASR字幕,再把字幕压制进视频,整个流程全自动化。

项目背景

我们团队做教育类短视频,有个痛点:老师讲课的视频,ASR识别出来的字幕经常和标准教案对不上。

举个例子,老师说“教技术”,ASR可能识别成“叫技术”。一个视频几百条字幕,一条条改太费时间。

于是有了这个项目的想法——用标准文案自动修正ASR字幕

整体架构

先上架构图:

视频文件
    ↓
提取音频 (FFmpeg)
    ↓
上传云存储 (阿里云OSS)
    ↓
ASR识别 (腾讯云MPS)
    ↓
生成SRT字幕
    ↓
文案修正 (滑动窗口匹配算法)
    ↓
生成首帧标题 (Gemini API)
    ↓
视频嵌入字幕 (FFmpeg overlay)
    ↓
输出成品视频

技术栈:

  • 云服务:腾讯云MPS(音视频处理)、OSS(存储)、Gemini API(标题生成)

  • 本地处理:FFmpeg(音视频编解码)、Python + pysrt(字幕处理)

  • 任务队列:Redis(异步处理)

核心模块实现

1. ASR字幕生成

调用腾讯云MPS的智能字幕接口,这部分比较直接。主要处理两个问题:

接口调用

from tencentcloud.mps.v20190612 import mps_client, models

def submit_asr_task(video_url: str) -> str:
    """提交ASR任务,返回TaskId"""
    req = models.ProcessMediaRequest()
    req.InputInfo = {
        "Type": "URL",
        "UrlInputInfo": {"Url": video_url}
    }
    req.AiRecognitionTask = {
        "Definition": 10  # 智能字幕模板
    }
    resp = client.ProcessMedia(req)
    return resp.TaskId

轮询等待结果

MPS是异步接口,提交任务后要轮询查询状态。一开始我设的10秒查一次,后来发现长视频处理很慢,改成30秒更合理。

def wait_for_result(task_id: str, timeout: int = 3600):
    """轮询等待任务完成"""
    start_time = time.time()
    while time.time() - start_time < timeout:
        detail = query_task_detail(task_id)
        if detail.Status == "FINISH":
            return extract_subtitles(detail)
        elif detail.Status == "FAIL":
            raise Exception(f"任务失败: {detail.ErrCodeExt}")
        time.sleep(30)  # 等30秒再查
    raise TimeoutError("任务超时")

2. 文案修正——最核心的算法

这个模块花了我最多时间。一开始想得很简单:逐行对比不就完了?

实际试了才发现问题:

  • ASR识别的断句和文案不一致

  • 有时候一行字幕对应文案的两行

  • 漏字、多字的情况很常见

最终方案:顺序滑动窗口匹配

思路是把整篇文案当成一个长字符串,ASR字幕逐行去里面找对应位置。

class ScriptCorrector:
    def __init__(self, script_text: str, threshold: float = 0.6):
        # 清理文案:去掉所有标点和空白
        self.clean_script = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9]', '', script_text)
        self.threshold = threshold
        self.current_pos = 0  # 当前匹配位置

    def correct_line(self, asr_text: str) -> str:
        """修正单行字幕"""
        clean_asr = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9]', '', asr_text)
        target_len = len(clean_asr)

        best_match = None
        best_score = 0

        # 在当前位置附近搜索,容错范围50个字符
        search_start = max(0, self.current_pos - 50)
        search_end = min(len(self.clean_script), self.current_pos + target_len + 50)

        # 变长窗口:尝试 target_len ± 2
        for window_len in range(target_len - 2, target_len + 3):
            if window_len <= 0:
                continue
            for i in range(search_start, search_end - window_len + 1):
                candidate = self.clean_script[i:i + window_len]
                score = self._similarity(clean_asr, candidate)

                # 位置越近,加分越多
                distance_penalty = abs(i - self.current_pos) / 100
                adjusted_score = score - distance_penalty

                if adjusted_score > best_score:
                    best_score = adjusted_score
                    best_match = candidate
                    best_pos = i

        if best_score >= self.threshold and best_match:
            self.current_pos = best_pos + len(best_match)
            return best_match
        else:
            return asr_text  # 匹配失败,保留原文

相似度计算

用的是编辑距离(Levenshtein distance),加上长度惩罚:

def _similarity(self, s1: str, s2: str) -> float:
    """计算两个字符串的相似度"""
    if not s1 or not s2:
        return 0.0

    # 编辑距离
    distance = self._levenshtein(s1, s2)
    max_len = max(len(s1), len(s2))

    # 基础相似度
    base_sim = 1 - distance / max_len

    # 长度差异惩罚
    len_ratio = min(len(s1), len(s2)) / max(len(s1), len(s2))

    return base_sim * len_ratio

阈值选择

经过测试,0.6是个不错的阈值:

  • 太高(0.8):很多只差一个字的匹配失败

  • 太低(0.4):容易匹配到错误的位置

0.6能处理“叫技术”→“教技术”这种单字差异。

3. 首帧标题生成

这个功能是后来加的。短视频需要在前几秒显示标题,吸引用户。

问题1:用什么生成标题?

一开始想用GPT,后来发现Gemini的价格更低,而且响应速度还行。

import google.generativeai as genai

def generate_title(srt_content: str) -> tuple[str, str]:
    """用Gemini生成两行标题"""
    prompt = f"""
    根据以下字幕内容,生成视频标题。
    要求:
    1. 两行标题
    2. 每行不超过10个汉字
    3. 简洁有力,吸引眼球

    字幕内容:
    {srt_content[:500]}  # 只取前500字符,够用了
    """

    response = model.generate_content(prompt)
    lines = response.text.strip().split('\n')
    return lines[0], lines[1] if len(lines) > 1 else ""

问题2:怎么把标题加到视频上?

一开始想用FFmpeg的drawtext滤镜:

ffmpeg -i input.mp4 -vf "drawtext=text='标题':fontfile=font.ttf" output.mp4

结果各种问题:

  • Windows和Linux字体路径不一样

  • 中文字体渲染有时候会乱码

  • 每换一台机器都要重新配字体

最后改用先生成图片,再用overlay叠加的方案:

from PIL import Image, ImageDraw, ImageFont

def create_title_image(title1: str, title2: str, video_width: int, video_height: int):
    """生成标题图片"""
    # 创建透明背景
    img = Image.new('RGBA', (video_width, video_height), (0, 0, 0, 0))
    draw = ImageDraw.Draw(img)

    # 自适应字体大小
    base_font_size = int(80 * video_width / 1920)
    font_size = max(50, min(150, base_font_size))
    font = ImageFont.truetype("msyh.ttc", font_size)

    # 计算位置(底部居中)
    y_position = video_height - 200

    # 画半透明背景条
    # ... 绘制逻辑

    # 画文字
    draw.text((x, y_position), title1, font=font, fill=(255, 255, 0))

    return img

然后用FFmpeg的overlay叠加:

def add_title_overlay(input_video: str, title_image: str, output_video: str, duration: float = 3.0):
    """叠加标题图片到视频开头"""
    cmd = [
        "ffmpeg", "-i", input_video, "-i", title_image,
        "-filter_complex", f"[0:v][1:v]overlay=0:0:enable='between(t,0,{duration})'",
        "-c:a", "copy",
        output_video
    ]
    subprocess.run(cmd, check=True)

这个方案的好处是跨平台兼容性好,字体用PIL处理,FFmpeg只负责叠加。

4. 字幕嵌入视频

最后一步,把SRT字幕压制到视频里。

def embed_subtitles(video_path: str, srt_path: str, output_path: str):
    """将字幕嵌入视频"""
    # 先把SRT转成ASS,可以自定义样式
    ass_path = convert_srt_to_ass(srt_path)

    cmd = [
        "ffmpeg", "-i", video_path,
        "-vf", f"ass={ass_path}",
        "-c:v", "libx264",
        "-preset", "fast",
        "-crf", "23",
        "-c:a", "aac",
        "-b:a", "128k",
        output_path
    ]
    subprocess.run(cmd, check=True)

为什么要转ASS?因为ASS格式支持更丰富的样式,字幕位置、颜色、描边都能自定义。

踩过的坑

坑1:pysrt在Python 3.11报错

pysrt.open("subtitles.srt")  # ValueError: invalid mode: 'rU'

原因是pysrt内部用了已废弃的'rU'模式,Python 3.11移除了这个模式。

解决方案:手动读取文件内容,再用pysrt.from_string()解析。

# 错误写法
srt_file = pysrt.open(srt_path)

# 正确写法
with open(srt_path, 'r', encoding='utf-8') as f:
    content = f.read()
srt_file = pysrt.from_string(content)

坑2:FFmpeg处理中文路径

Windows下,如果路径有中文,FFmpeg可能识别不了。

解决方案:要么用英文路径,要么设置编码:

import subprocess
result = subprocess.run(cmd,
    check=True,
    encoding='utf-8',
    env={**os.environ, 'LANG': 'zh_CN.UTF-8'}
)

坑3:腾讯云MPS的回调地址

MPS支持设置回调URL,任务完成后主动通知。但如果服务在内网,接收不到回调。

最后还是用轮询方案,简单粗暴但可靠。

项目结构

intelligent_srt/
├── main.py                 # 主程序入口
├── subtitle_processor.py   # 字幕处理(修正、对比)
├── video_processor.py      # 视频处理(下载、分割、嵌入)
├── mps_api.py             # 腾讯云MPS接口封装
├── workflow.py            # 完整工作流编排
├── config.py              # 配置管理
├── redis_task_processor.py # Redis任务监听
├── oss_client.py          # 阿里云OSS客户端
├── .env                   # 环境变量配置
└── requirements.txt       # 依赖列表

配置说明

项目用.env文件管理配置:

# 腾讯云
TENCENT_SECRET_ID=your_secret_id
TENCENT_SECRET_KEY=your_secret_key
MPS_REGION=ap-shanghai

# 阿里云OSS
OSS_ACCESS_KEY_ID=your_key_id
OSS_ACCESS_KEY_SECRET=your_key_secret
OSS_BUCKET=your_bucket
OSS_ENDPOINT=oss-cn-shanghai.aliyuncs.com

# Gemini
GEMINI_API_KEY=your_api_key

# 字幕修正
MATCHING_THRESHOLD=0.6  # 相似度阈值

# Redis(可选,异步处理用)
REDIS_HOST=localhost
REDIS_PORT=6379

性能数据

在我的测试环境(4核8G云服务器):

视频时长

ASR识别

文案修正

字幕嵌入

总耗时

1分钟

30秒

<1秒

10秒

~45秒

5分钟

2分钟

2秒

40秒

~3分钟

10分钟

4分钟

3秒

1.5分钟

~6分钟

瓶颈在ASR识别和视频编码,文案修正很快。

后续优化方向

  1. 支持多语言:目前只支持中文,后续可以加英文、日文

  2. 批量处理:用Redis队列已经支持了,但还可以优化并发数

  3. 字幕样式模板:让用户自定义字幕样式

  4. Web界面:目前只有命令行,可以加个简单的前端

总结

这个项目解决了我们团队的实际痛点,核心思路是:

  1. 用云服务处理重活:ASR识别交给腾讯云,不用自己训练模型

  2. 本地处理轻活:文案修正、字幕格式转换这些用Python就够了

  3. FFmpeg是万能的:音视频处理绑定FFmpeg,跨平台无忧

最花时间的是文案修正算法,滑动窗口匹配+编辑距离+位置偏好,调参调了挺久。


如果觉得有帮助,欢迎star和fork。有问题可以提issue讨论。

上一篇 下一篇
评论
来首音乐
光阴似箭
今日已经过去小时
这周已经过去
本月已经过去
今年已经过去个月
文章目录
每日一句