铸渊 ICE-GL-ZY001 a2e5214f03 LL-172-20260707 · cang-ying 仓初始化 · 苍耳+鉴影的干净之家
铸渊 ICE-GL-ZY001
LL-172-20260707
冰朔委托: 新建第 5 子仓, 给苍耳(人类主控) + 鉴影(人格体) 专用
原 guanghulab/video-ai-system/ 东西太多(225 文件) · 找不到 · 乱

迁移:
  ⊢ 16 个核心 .hdlp (VA-GATE / VA-LIGHTHOUSE / VA-BROADCAST / VA-SYSTEM-STATUS 等)
  ⊢ 17 个子目录 (agents/engines/protocols/tasks/tools/assets/knowledge/memory/docs/config/brain/director-brain/experience/feedback/issues/plans/reference-analysis)

排除:
  ⊢ outputs/ (视频产物)
  ⊢ test-input/ test-output/ (测试)
  ⊢ data/ (临时数据)
  ⊢ preview-001/002 (旧产片)
  ⊢ 旧分镜/旧提示词/旧导演编码

后续:
  ⊢ 老仓 guanghulab/video-ai-system/ 改写为已迁出占位
  ⊢ 苍耳+鉴影 写新东西进本仓
  ⊢ GLOBAL-SEARCH 加 cang-ying 仓库

铸渊 ICE-GL-ZY001 · 2026-07-07 D167
冰朔 ICE-GL∞ 主权
2026-07-07 10:20:10 +08:00

401 lines
14 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
"""
ASS Subtitle Renderer · ASS字幕渲染器
=====================================
用 ASS/libass 正式渲染字幕,支持字体、粗体、描边、边距、对齐。
不再用 Pillow PNG 凑合。
依赖:
pip install pysrt
# FFmpeg 需要编译时启用 libass 支持(通常默认启用)
用法:
# 生成 ASS 字幕文件
python ass-subtitle-renderer.py --srt input.srt --style reference-drama --output subtitles.ass
# 用 FFmpeg + libass 烧字幕到视频
python ass-subtitle-renderer.py --srt input.srt --video input.mp4 --output output.mp4 --style reference-drama
# 指定自定义样式配置
python ass-subtitle-renderer.py --srt input.srt --config custom-style.json --output subtitles.ass
# 批量处理多集
python ass-subtitle-renderer.py --batch ./srt/ --style reference-drama --output-dir ./ass/
输出:
- ASS 字幕文件(可直接用 FFmpeg 烧录)
- 烧录字幕后的视频(可选)
路径:
video-ai-system/engines/subtitle-pipeline/reference-analysis/ass-subtitle-renderer.py
"""
import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
try:
import pysrt
except ImportError:
print("[ERROR] 缺少依赖pysrt")
print("请先安装pip install pysrt")
sys.exit(1)
# 默认样式配置(参考短剧风格)
DEFAULT_STYLE = {
"video_width": 1080,
"video_height": 1920,
"font_family": "PingFang SC",
"font_size": 38,
"font_color": "&HFFFFFF&", # 白色ASS格式&HBBGGRR&
"font_color_hex": "#FFFFFF",
"bold": True,
"italic": False,
"underline": False,
"strikeout": False,
"stroke_enabled": True,
"stroke_width": 2,
"stroke_color": "&H000000&", # 黑色描边
"stroke_opacity": 0.92, # &H40&0-2550=透明255=不透明)
"shadow_enabled": False,
"shadow_depth": 0,
"alignment": 2, # 2=中下
"margin_left": 100,
"margin_right": 100,
"margin_vertical": 50,
"line_spacing": 1.45,
"letter_spacing": 0,
"background_enabled": False,
"fade_in_ms": 150,
"fade_out_ms": 150,
}
def load_style_config(config_path: str = None, style_name: str = "reference-drama") -> dict:
"""
加载样式配置
:param config_path: 自定义配置文件路径
:param style_name: 预设样式名称
:return: 样式字典
"""
if config_path and os.path.isfile(config_path):
with open(config_path, "r", encoding="utf-8") as f:
config = json.load(f)
print(f"[INFO] 已加载自定义样式配置:{config_path}")
return config
# 尝试从 assets/subtitle-styles/ 加载预设样式
style_file = os.path.join(
os.path.dirname(__file__),
"..", "..", "..", "assets", "subtitle-styles",
f"subtitle-style.{style_name}.json"
)
style_file = os.path.abspath(style_file)
if os.path.isfile(style_file):
with open(style_file, "r", encoding="utf-8") as f:
config = json.load(f)
print(f"[INFO] 已加载预设样式:{style_name}")
return config
print(f"[WARN] 未找到样式配置,使用默认样式")
return DEFAULT_STYLE
def srt_time_to_ass(srt_time) -> str:
"""
将 pysrt 时间对象转换为 ASS 时间戳格式
:param srt_time: pysrt 时间对象
:return: ASS 时间戳字符串H:MM:SS.cc
"""
hours = srt_time.hours
minutes = srt_time.minutes
seconds = srt_time.seconds
milliseconds = srt_time.milliseconds
# ASS 时间戳格式H:MM:SS.cccc=厘秒1/100秒
centiseconds = milliseconds // 10
return f"{hours}:{minutes:02d}:{seconds:02d}.{centiseconds:02d}"
def generate_ass_file(srt_path: str, output_path: str, style: dict) -> bool:
"""
将 SRT 字幕转换为 ASS 格式
:param srt_path: SRT 文件路径
:param output_path: 输出 ASS 文件路径
:param style: 样式字典
:return: 是否成功
"""
if not os.path.isfile(srt_path):
print(f"[ERROR] SRT 文件不存在:{srt_path}")
return False
# 加载 SRT
try:
subs = pysrt.open(srt_path, encoding="utf-8")
except Exception as e:
print(f"[ERROR] 加载 SRT 失败:{e}")
return False
print(f"[INFO] 找到 {len(subs)} 条字幕,开始生成 ASS 文件...")
# 构建 ASS 文件内容
ass_lines = []
# 1. [Script Info] 节
ass_lines.append("[Script Info]")
ass_lines.append("; Script generated by ASS Subtitle Renderer")
ass_lines.append(f"PlayResX: {style.get('video_width', 1080)}")
ass_lines.append(f"PlayResY: {style.get('video_height', 1920)}")
ass_lines.append("Aspect Ratio: 9:16")
ass_lines.append("")
# 2. [V4+ Styles] 节
ass_lines.append("[V4+ Styles]")
ass_lines.append(
"Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, "
"Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, "
"BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding"
)
# 构建样式行
font_name = style.get("font_family", "PingFang SC")
font_size = style.get("font_size", 38)
primary_colour = style.get("font_color", "&HFFFFFF&")
outline_colour = style.get("stroke_color", "&H000000&")
# 背景色(如果启用背景)
if style.get("background_enabled", False):
bg_opacity = int(style.get("background_opacity", 0.6) * 255)
back_colour = f"&H{bg_opacity:02X}000000&" # &HAAKKBBDD&
else:
back_colour = "&H00000000&" # 透明
bold = -1 if style.get("bold", True) else 0
italic = -1 if style.get("italic", False) else 0
underline = -1 if style.get("underline", False) else 0
strikeout = -1 if style.get("strikeout", False) else 0
outline_width = style.get("stroke_width", 2) if style.get("stroke_enabled", True) else 0
shadow_depth = style.get("shadow_depth", 0) if style.get("shadow_enabled", False) else 0
alignment = style.get("alignment", 2)
margin_l = style.get("margin_left", 100)
margin_r = style.get("margin_right", 100)
margin_v = style.get("margin_vertical", 50)
style_line = (
f"Style: Default,{font_name},{font_size},{primary_colour},&H000000FF&,{outline_colour},{back_colour},"
f"{bold},{italic},{underline},{strikeout},100,100,{style.get('letter_spacing', 0)},0,"
f"1,{outline_width},{shadow_depth},{alignment},{margin_l},{margin_r},{margin_v},1"
)
ass_lines.append(style_line)
ass_lines.append("")
# 3. [Events] 节
ass_lines.append("[Events]")
ass_lines.append("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text")
for sub in subs:
start_time = srt_time_to_ass(sub.start)
end_time = srt_time_to_ass(sub.end)
text = sub.text.strip()
# 添加淡入淡出效果(如果启用)
fade_in = style.get("fade_in_ms", 150)
fade_out = style.get("fade_out_ms", 150)
if fade_in > 0 and fade_out > 0:
# ASS 淡入淡出标签:{\fad(fade_in,fade_out)}
effect_tags = f"{{\\fad({fade_in},{fade_out})}}"
else:
effect_tags = ""
# 转义特殊字符ASS 格式)
text = text.replace("\\", "\\\\")
text = text.replace("{", "\\{")
text = text.replace("}", "\\}")
text = text.replace("\n", "\\N") # ASS 换行符
# 构建对话行
dialogue_line = (
f"Dialogue: 0,{start_time},{end_time},Default,,0,0,0,,{effect_tags}{text}"
)
ass_lines.append(dialogue_line)
# 写入 ASS 文件
try:
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
with open(output_path, "w", encoding="utf-8-sig") as f: # UTF-8 BOM for ASS
f.write("\n".join(ass_lines))
print(f"[OK] ASS 字幕文件已生成:{output_path}")
print(f"[INFO] 共 {len(subs)} 条字幕")
return True
except Exception as e:
print(f"[ERROR] 写入 ASS 文件失败:{e}")
return False
def burn_subtitles_with_ffmpeg(video_path: str, ass_path: str, output_path: str) -> bool:
"""
用 FFmpeg + libass 烧录字幕到视频
:param video_path: 输入视频路径
:param ass_path: ASS 字幕文件路径
:param output_path: 输出视频路径
:return: 是否成功
"""
if not os.path.isfile(video_path):
print(f"[ERROR] 视频文件不存在:{video_path}")
return False
if not os.path.isfile(ass_path):
print(f"[ERROR] ASS 字幕文件不存在:{ass_path}")
return False
# 使用 subtitles 滤镜(更通用,支持 SRT/ASS 等多种格式)
# 正确语法subtitles=filename='path' (需要 filename= 前缀)
ass_path_escaped = ass_path.replace(":", "\\:").replace("'", "'\\''")
cmd = [
"ffmpeg", "-y",
"-i", video_path,
"-vf", f"subtitles=filename='{ass_path_escaped}'",
"-c:v", "libx264",
"-pix_fmt", "yuv420p",
"-c:a", "copy",
"-shortest",
output_path
]
print(f"[INFO] 开始用 FFmpeg + libass 烧录字幕...")
print(f"[INFO] 命令:{' '.join(cmd[:10])}...") # 只打印前10个参数
try:
result = subprocess.run(cmd, check=False, capture_output=True, text=True)
except FileNotFoundError:
print("[ERROR] 未找到 ffmpeg请先安装 FFmpeg")
print("安装方法:")
print(" macOS: brew install ffmpeg")
print(" Ubuntu: sudo apt install ffmpeg")
return False
if result.returncode != 0:
print("[ERROR] FFmpeg 烧录字幕失败")
print(result.stderr[-2000:]) # 打印最后2000字符的错误信息
return False
if os.path.isfile(output_path) and os.path.getsize(output_path) > 0:
print(f"[OK] 字幕已烧录到视频:{output_path}")
return True
print(f"[ERROR] 输出视频未生成:{output_path}")
return False
def batch_process(srt_dir: str, output_dir: str, style: dict, burn_video: bool = False, video_dir: str = None):
"""
批量处理 SRT 文件
:param srt_dir: SRT 文件目录
:param output_dir: 输出目录
:param style: 样式字典
:param burn_video: 是否烧录到视频
:param video_dir: 视频文件目录(如果 burn_video=True
"""
os.makedirs(output_dir, exist_ok=True)
for file in os.listdir(srt_dir):
if file.lower().endswith(".srt"):
srt_path = os.path.join(srt_dir, file)
ass_output = os.path.join(output_dir, file.replace(".srt", ".ass"))
print(f"\n[INFO] 处理文件:{file}")
ok = generate_ass_file(srt_path, ass_output, style)
if ok and burn_video and video_dir:
# 查找对应的视频文件
video_file = file.replace(".srt", ".mp4")
video_path = os.path.join(video_dir, video_file)
if os.path.isfile(video_path):
video_output = os.path.join(output_dir, video_file)
burn_subtitles_with_ffmpeg(video_path, ass_output, video_output)
else:
print(f"[WARN] 未找到对应的视频文件:{video_file}")
def main():
parser = argparse.ArgumentParser(
description="ASS Subtitle Renderer · ASS字幕渲染器",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
python ass-subtitle-renderer.py --srt input.srt --style reference-drama --output subtitles.ass
python ass-subtitle-renderer.py --srt input.srt --video input.mp4 --output output.mp4 --style reference-drama
python ass-subtitle-renderer.py --batch ./srt/ --style reference-drama --output-dir ./ass/
"""
)
parser.add_argument("--srt", help="输入 SRT 文件路径")
parser.add_argument("--video", help="输入视频路径(可选,用于直接烧录字幕)")
parser.add_argument("--output", help="输出文件路径ASS 或 MP4")
parser.add_argument("--style", default="reference-drama", help="样式名称或配置文件路径")
parser.add_argument("--config", help="自定义样式配置文件路径(覆盖 --style")
parser.add_argument("--batch", help="批量处理 SRT 文件目录")
parser.add_argument("--output-dir", help="批量处理输出目录")
parser.add_argument("--burn-video", action="store_true", help="是否烧录字幕到视频")
parser.add_argument("--video-dir", help="视频文件目录(配合 --burn-video 使用)")
args = parser.parse_args()
# 加载样式配置
if args.config:
style = load_style_config(config_path=args.config)
else:
style = load_style_config(style_name=args.style)
if args.srt:
# 单文件处理
if not args.output:
print("[ERROR] 请指定 --output")
sys.exit(1)
# 生成 ASS 文件
ok = generate_ass_file(args.srt, args.output, style)
if not ok:
sys.exit(1)
# 如果指定了视频,则烧录字幕
if args.video:
video_output = args.output.replace(".ass", ".mp4")
burn_ok = burn_subtitles_with_ffmpeg(args.video, args.output, video_output)
if not burn_ok:
sys.exit(1)
elif args.batch:
# 批量处理
if not args.output_dir:
print("[ERROR] 批量处理需要指定 --output-dir")
sys.exit(1)
batch_process(args.batch, args.output_dir, style, args.burn_video, args.video_dir)
else:
print("[ERROR] 请指定 --srt 或 --batch")
sys.exit(1)
print("\n[OK] 处理完成")
if __name__ == "__main__":
main()