guanghulab/video-ai-system/engines/subtitle-renderer.py
冰朔 1eaa2b532f
Some checks failed
自动更新代码和重启 / update-and-restart (push) Has been cancelled
CI检查 + 自动部署 / check (push) Has been cancelled
CI检查 + 自动部署 / deploy (push) Has been cancelled
D144: 配音+字幕管线开发·Agent可直接调用
- 新增 engines/tts-engine.py:Edge-TTS配音引擎,支持多角色音色配置
- 新增 engines/subtitle-renderer.py:字幕渲染引擎,SRT→PNG序列
- 更新 ENTRY.hdlp:工具链增加 tts-engine.py 和 subtitle-renderer.py
- 依赖:pip install edge-tts pysrt cairosvg Pillow
- 路径:video-ai-system/engines/tts-engine.py
2026-06-23 23:18:01 +08:00

320 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()