e9dbf4f5fc
- 新增 generate_empty_shot_clip 原子命令:截取视频→截取音频→替换音频→自动清理临时文件 - 新增 concat_video_clips 命令:直接拼接已标准化片段,零重新编码 - VideoGeneration 改为先生成各分镜标准化片段,再 concat 拼接,不再使用 replace_audio_track - segment 对口型视频保留自带同步音频,empty_shot 注入对应配音音频 - 删除未使用的单分镜重新生成功能(handleRegenerateShot、useVideoGeneration hook) - ScriptShot 新增 clipVideoPath 字段
126 lines
3.7 KiB
Python
126 lines
3.7 KiB
Python
"""
|
||
ASS 字幕生成器
|
||
==============
|
||
|
||
生成带样式的 ASS 字幕文件,使用抖音美好体 (DouyinSans)。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from app.schemas.caption import CaptionUtterance
|
||
|
||
|
||
def generate_ass(
|
||
utterances: list[CaptionUtterance],
|
||
video_width: int = 1080,
|
||
video_height: int = 1920,
|
||
fontname: str = "DouyinSansBold",
|
||
fontsize: float = 28.0,
|
||
primary_color: str = "&H00FFFFFF", # 白色
|
||
outline_color: str = "&H00000000", # 黑色描边
|
||
back_color: str = "&H80000000", # 半透明背景
|
||
outline: float = 2.0,
|
||
shadow: float = 1.0,
|
||
margin_v: int = 50,
|
||
border_style: int = 1,
|
||
) -> str:
|
||
"""
|
||
生成 ASS 字幕内容
|
||
|
||
Args:
|
||
utterances: 字幕时间轴列表
|
||
video_width: 视频宽度
|
||
video_height: 视频高度
|
||
fontname: 字体名称(默认为 DouyinSansBold)
|
||
fontsize: 字体大小
|
||
primary_color: 主颜色 (&HAABBGGRR 格式)
|
||
outline_color: 描边颜色
|
||
back_color: 背景颜色
|
||
outline: 描边宽度
|
||
shadow: 阴影深度
|
||
margin_v: 垂直边距
|
||
border_style: 边框样式 (1=描边, 3=背景色块)
|
||
|
||
Returns:
|
||
ASS 格式字符串
|
||
"""
|
||
if border_style not in (1, 3):
|
||
border_style = 1
|
||
|
||
# BorderStyle=3 在当前 FFmpeg 嵌入版 libass 中不渲染,
|
||
# 转换为 BorderStyle=1 + 粗描边来模拟背景色效果
|
||
if border_style == 3:
|
||
border_style = 1
|
||
outline = max(6, round(fontsize * 0.12))
|
||
outline_color = back_color
|
||
|
||
# Script Info 部分
|
||
script_info = f"""[Script Info]
|
||
Title: Generated by Meijiaka AI Video
|
||
ScriptType: v4.00+
|
||
PlayResX: {video_width}
|
||
PlayResY: {video_height}
|
||
ScaledBorderAndShadow: yes
|
||
YCbCr Matrix: None
|
||
|
||
"""
|
||
|
||
# Styles 部分
|
||
styles = f"""[V4+ Styles]
|
||
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
||
Style: Default,{fontname},{fontsize:.1f},{primary_color},&H000000FF,{outline_color},{back_color},0,0,0,0,100,100,0,0,{border_style},{outline:.1f},{shadow:.1f},2,20,20,{margin_v},1
|
||
|
||
"""
|
||
|
||
# Events 部分
|
||
events_header = """[Events]
|
||
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
||
"""
|
||
|
||
events = []
|
||
for u in utterances:
|
||
start = _ms_to_ass_time(u.start_time)
|
||
end = _ms_to_ass_time(u.end_time)
|
||
# 转义特殊字符
|
||
text = u.text.replace("\\", "\\\\").replace("{", "\\{").replace("}", "\\}")
|
||
event = f"Dialogue: 0,{start},{end},Default,,0,0,0,,{text}"
|
||
events.append(event)
|
||
|
||
return script_info + styles + events_header + "\n".join(events)
|
||
|
||
|
||
def _ms_to_ass_time(ms: int) -> str:
|
||
"""毫秒转换为 ASS 时间格式 H:MM:SS.cc"""
|
||
hours = ms // 3600000
|
||
minutes = (ms % 3600000) // 60000
|
||
seconds = (ms % 60000) // 1000
|
||
centiseconds = (ms % 1000) // 10
|
||
return f"{hours}:{minutes:02d}:{seconds:02d}.{centiseconds:02d}"
|
||
|
||
|
||
# 预设样式
|
||
def generate_ass_short_video(utterances: list[CaptionUtterance]) -> str:
|
||
"""生成短视频风格的 ASS 字幕(竖屏 9:16)"""
|
||
return generate_ass(
|
||
utterances=utterances,
|
||
video_width=1080,
|
||
video_height=1920,
|
||
fontsize=32.0,
|
||
outline=2.5,
|
||
shadow=2.0,
|
||
margin_v=80,
|
||
)
|
||
|
||
|
||
def generate_ass_professional(utterances: list[CaptionUtterance]) -> str:
|
||
"""生成专业风格的 ASS 字幕(横屏 16:9)"""
|
||
return generate_ass(
|
||
utterances=utterances,
|
||
video_width=1920,
|
||
video_height=1080,
|
||
fontsize=24.0,
|
||
outline=1.5,
|
||
shadow=1.0,
|
||
margin_v=60,
|
||
)
|