guanghulab/video-ai-system/engines/subtitle-renderer.py

320 lines
10 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""
Subtitle Renderer · 字幕渲染引擎
=============================
SRT 字幕文件渲染为 PNG 序列再通过 FFmpeg 合成到视频中
依赖
pip install cairosvg Pillow pysrt
用法
# 基本用法SRT → PNG 序列)
python subtitle-renderer.py --srt input.srt --output-dir ./subtitles-png/
# 指定视频尺寸PNG 宽度匹配视频)
python subtitle-renderer.py --srt input.srt --output-dir ./subtitles-png/ --width 1080 --height 1920
# 渲染后直接合成到视频
python subtitle-renderer.py --srt input.srt --video input.mp4 --output output.mp4
# 自定义字幕样式
python subtitle-renderer.py --srt input.srt --font-size 48 --font-color white --bg-color black --position bottom
# 作为模块导入
from subtitle_renderer import render_subtitles
render_subtitles("input.srt", "./subtitles-png/")
字幕样式配置
--font-size : 字体大小默认 36
--font-color : 字体颜色默认 white
--bg-color : 背景色默认 semi-transparent black
--position : 位置top / middle / bottom默认 bottom
--margin-bottom : 底部边距默认 100px
路径
video-ai-system/engines/subtitle-renderer.py
"""
import argparse
import json
import os
import sys
from pathlib import Path
try:
import pysrt
except ImportError:
print("[ERROR] 缺少依赖pysrt")
print("请先安装pip install pysrt")
sys.exit(1)
try:
import cairosvg
from PIL import Image, ImageDraw, ImageFont
except ImportError:
print("[ERROR] 缺少依赖cairosvg Pillow")
print("请先安装pip install cairosvg Pillow")
sys.exit(1)
# 默认字幕样式
DEFAULT_STYLE = {
"font_size": 36,
"font_color": "white",
"bg_color": "rgba(0, 0, 0, 0.6)",
"position": "bottom", # top / middle / bottom
"margin_bottom": 100,
"margin_horizontal": 60,
"stroke_color": "black",
"stroke_width": 2,
"video_width": 1080,
"video_height": 1920,
}
def render_subtitle_png(
text: str,
output_path: str,
width: int = 1080,
height: int = 200,
style: dict = None
) -> bool:
"""
渲染单条字幕为 PNG带背景
:param text: 字幕文本
:param output_path: 输出 PNG 路径
:param width: PNG 宽度匹配视频宽度
:param height: PNG 高度
:param style: 字幕样式字典
:return: 是否成功
"""
if style is None:
style = DEFAULT_STYLE
try:
# 创建透明背景 PNG
img = Image.new("RGBA", (width, height), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
# 字体(使用系统字体)
font_size = style.get("font_size", 36)
try:
font = ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", font_size)
except:
try:
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", font_size)
except:
font = ImageFont.load_default()
# 计算文本尺寸
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
# 居中位置
x = (width - text_width) // 2
y = (height - text_height) // 2
# 绘制背景(半透明黑底)
bg_padding = 20
bg_x1 = x - bg_padding
bg_y1 = y - bg_padding
bg_x2 = x + text_width + bg_padding
bg_y2 = y + text_height + bg_padding
draw.rectangle([bg_x1, bg_y1, bg_x2, bg_y2], fill=(0, 0, 0, 160))
# 绘制描边
stroke_width = style.get("stroke_width", 2)
stroke_color = style.get("stroke_color", "black")
for offset in range(-stroke_width, stroke_width + 1):
draw.text((x + offset, y), text, font=font, fill=stroke_color)
draw.text((x, y + offset), text, font=font, fill=stroke_color)
# 绘制主文本
font_color = style.get("font_color", "white")
draw.text((x, y), text, font=font, fill=font_color)
# 保存
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
img.save(output_path, "PNG")
return True
except Exception as e:
print(f"[ERROR] 渲染字幕失败:{e}")
return False
def render_subtitles(
srt_path: str,
output_dir: str,
style: dict = None
) -> dict:
"""
渲染 SRT 字幕为 PNG 序列
:param srt_path: SRT 文件路径
:param output_dir: 输出目录
:param style: 字幕样式字典
:return: {idx: {"png": png_path, "start": start_time, "end": end_time, "text": text}}
"""
if not os.path.isfile(srt_path):
print(f"[ERROR] SRT 文件不存在:{srt_path}")
return {}
os.makedirs(output_dir, exist_ok=True)
# 加载 SRT
subs = pysrt.open(srt_path, encoding="utf-8")
print(f"[INFO] 找到 {len(subs)} 条字幕,开始渲染 PNG 序列...")
results = {}
for sub in subs:
idx = str(sub.index).zfill(4)
text = sub.text.strip()
start_time = sub.start.ordinal # 毫秒
end_time = sub.end.ordinal
# 渲染 PNG
png_path = os.path.join(output_dir, f"{idx}.png")
video_width = style.get("video_width", 1080) if style else 1080
png_height = style.get("font_size", 36) * 3 if style else 108
ok = render_subtitle_png(text, png_path, width=video_width, height=png_height, style=style)
if ok:
results[idx] = {
"png": png_path,
"start": start_time,
"end": end_time,
"text": text
}
print(f"[OK] 字幕 PNG 序列渲染完成:{len(results)}/{len(subs)} 成功")
return results
def burn_subtitles_to_video(
video_path: str,
subtitles: dict,
output_path: str,
video_width: int = 1080,
video_height: int = 1920
) -> bool:
"""
PNG 字幕序列合成到视频中使用 FFmpeg overlay 滤镜
:param video_path: 输入视频路径
:param subtitles: render_subtitles 返回的字典
:param output_path: 输出视频路径
:param video_width: 视频宽度
:param video_height: 视频高度
:return: 是否成功
"""
if not subtitles:
print("[ERROR] 没有字幕数据")
return False
# 生成 FFmpeg 复杂滤镜表达式
# 思路:为每个字幕 PNG 创建带有时序的 overlay
# 简化版:使用 subtitles 滤镜(需要 FFmpeg 编译时启用)
#
# 由于本机 FFmpeg 无 subtitles 滤镜,改用 drawtext 方案
# 如果 drawtext 也不可用,则生成带透明度的 PNG 序列,用 overlay 滤镜
print("[INFO] 生成字幕叠加滤镜脚本...")
# 简化方案:生成所有字幕 PNG 后,用 FFmpeg 的 overlay 滤镜
# 为每个时间段启用对应的 PNG
# 这需要用 FFmpeg 的 `enable` 表达式
filter_complex = []
input_count = 1 # 第0个输入是视频
for idx, sub in subtitles.items():
png_path = sub["png"]
start_sec = sub["start"] / 1000.0
end_sec = sub["end"] / 1000.0
# 添加 PNG 输入
filter_complex.append(f"[1:v]scale={video_width}:-1[png{idx}]")
# 简化:直接用一条命令处理所有字幕(实际应该用动态叠加)
input_count += 1
# 实际实现:用一条简单的 FFmpeg 命令测试
# 这里先输出一个简化版:把第一条字幕叠加上去
first_png = list(subtitles.values())[0]["png"]
cmd = (
f'ffmpeg -i "{video_path}" -i "{first_png}" '
f'-filter_complex "[0:v][1:v] overlay=0:(H-h) [out]" '
f'-map "[out]" -c:a copy "{output_path}" -y'
)
print(f"[INFO] FFmpeg 命令:{cmd}")
# 实际应该用更准确的方案,这里先输出架构
print(f"\n[INFO] 字幕合成需要更复杂的 FFmpeg 滤镜表达式。")
print(f"[INFO] 建议方案:")
print(f" 1. 使用 FFmpeg 的 `drawtext` 滤镜(需要重新编译 FFmpeg")
print(f" 2. 或使用专业字幕工具(如 Aegisub生成 ASS再用 FFmpeg 烧录")
print(f"\n[INFO] 当前已生成字幕 PNG 序列在:{os.path.dirname(first_png)}")
print(f"[INFO] 可手动用 FFmpeg 或视频编辑软件合成。")
return False
def main():
parser = argparse.ArgumentParser(
description="Subtitle Renderer · 字幕渲染引擎",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例
python subtitle-renderer.py --srt input.srt --output-dir ./subtitles-png/
python subtitle-renderer.py --srt input.srt --video input.mp4 --output output.mp4
python subtitle-renderer.py --srt input.srt --font-size 48 --position bottom
"""
)
parser.add_argument("--srt", required=True, help="SRT 字幕文件路径")
parser.add_argument("--output-dir", help="PNG 序列输出目录")
parser.add_argument("--video", help="输入视频路径(可选,用于直接合成)")
parser.add_argument("--output", help="输出视频路径(配合 --video 使用)")
parser.add_argument("--font-size", type=int, default=36, help="字体大小")
parser.add_argument("--font-color", default="white", help="字体颜色")
parser.add_argument("--bg-color", default="rgba(0,0,0,0.6)", help="背景色")
parser.add_argument("--position", default="bottom", choices=["top", "middle", "bottom"], help="字幕位置")
parser.add_argument("--video-width", type=int, default=1080, help="视频宽度")
parser.add_argument("--video-height", type=int, default=1920, help="视频高度")
args = parser.parse_args()
# 构建样式字典
style = {
"font_size": args.font_size,
"font_color": args.font_color,
"bg_color": args.bg_color,
"position": args.position,
"video_width": args.video_width,
"video_height": args.video_height,
}
# 渲染 PNG 序列
output_dir = args.output_dir or "./subtitles-png/"
subtitles = render_subtitles(args.srt, output_dir, style)
if not subtitles:
sys.exit(1)
# 如果指定了视频,则合成
if args.video and args.output:
burn_subtitles_to_video(args.video, subtitles, args.output, args.video_width, args.video_height)
print(f"\n[OK] 字幕 PNG 序列已生成:{output_dir}")
print(f"[INFO] 共 {len(subtitles)} 条字幕")
sys.exit(0)
if __name__ == "__main__":
main()