做短视频剪辑的朋友应该都遇到过这个问题:视频里的人说话,ASR识别出来的字幕总有些地方不对。错别字、漏字、多字,改起来很烦。这篇文章分享我最近做的一个项目,用标准文案自动修正ASR字幕,再把字幕压制进视频,整个流程全自动化。
我们团队做教育类短视频,有个痛点:老师讲课的视频,ASR识别出来的字幕经常和标准教案对不上。
举个例子,老师说“教技术”,ASR可能识别成“叫技术”。一个视频几百条字幕,一条条改太费时间。
于是有了这个项目的想法——用标准文案自动修正ASR字幕。
先上架构图:
视频文件
↓
提取音频 (FFmpeg)
↓
上传云存储 (阿里云OSS)
↓
ASR识别 (腾讯云MPS)
↓
生成SRT字幕
↓
文案修正 (滑动窗口匹配算法)
↓
生成首帧标题 (Gemini API)
↓
视频嵌入字幕 (FFmpeg overlay)
↓
输出成品视频
技术栈:
云服务:腾讯云MPS(音视频处理)、OSS(存储)、Gemini API(标题生成)
本地处理:FFmpeg(音视频编解码)、Python + pysrt(字幕处理)
任务队列:Redis(异步处理)
调用腾讯云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("任务超时")
这个模块花了我最多时间。一开始想得很简单:逐行对比不就完了?
实际试了才发现问题:
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能处理“叫技术”→“教技术”这种单字差异。
这个功能是后来加的。短视频需要在前几秒显示标题,吸引用户。
问题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只负责叠加。
最后一步,把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格式支持更丰富的样式,字幕位置、颜色、描边都能自定义。
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)
Windows下,如果路径有中文,FFmpeg可能识别不了。
解决方案:要么用英文路径,要么设置编码:
import subprocess
result = subprocess.run(cmd,
check=True,
encoding='utf-8',
env={**os.environ, 'LANG': 'zh_CN.UTF-8'}
)
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识别和视频编码,文案修正很快。
支持多语言:目前只支持中文,后续可以加英文、日文
批量处理:用Redis队列已经支持了,但还可以优化并发数
字幕样式模板:让用户自定义字幕样式
Web界面:目前只有命令行,可以加个简单的前端
这个项目解决了我们团队的实际痛点,核心思路是:
用云服务处理重活:ASR识别交给腾讯云,不用自己训练模型
本地处理轻活:文案修正、字幕格式转换这些用Python就够了
FFmpeg是万能的:音视频处理绑定FFmpeg,跨平台无忧
最花时间的是文案修正算法,滑动窗口匹配+编辑距离+位置偏好,调参调了挺久。
如果觉得有帮助,欢迎star和fork。有问题可以提issue讨论。