Files
vrsub/nodes/ass.py
T
cat-shark 2e8bbb15a4 feat: ASS字幕移到顶部安全区并加透明度(零视差不变)
- srt-to-dual-eye-ass: an2底部→an8顶部对齐,MarginV改为可配margin_top(默认120),
  字幕避开画面中央人脸高发区(B-1顶部安全区)
- 文字填充&H80FFFFFF→&HB3FFFFFF(约70%透明),描边纯黑→&H80000000半透明黑
- A-1零视差保持不变:左右眼水平相对位置一致,字幕固定屏幕平面
- 新增3条测试(顶部对齐/自定义margin_top/invoke读参数),ass节点覆盖率100%
- 新增docs/调查报告:双目视觉景深原理、字幕深度配置公式与遮挡解决方案
2026-09-05 21:03:55 +08:00

116 lines
5.3 KiB
Python
Executable File
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.
"""SRT 转 ASS 节点。
单体版中作为进程内节点模块,由调度器直接调用。解析标准 SRT 后生成 ASS
文件,其中同一句字幕同时输出 LeftEye 与 RightEye 两个样式,分别落在屏幕
左右两半,形成 VR 双眼叠加效果。
"""
from __future__ import annotations
from pathlib import Path
from wov_sdk.models import InvokeRequest, InvokeResponse
def _ass_header(resolution: str, margin_top: int = 120) -> str:
"""生成 ASS 文件头:脚本信息、左右眼样式和事件格式。
样式说明(B-1 顶部安全区 + 透明度):
- 对齐 8(顶部居中,\an8),MarginV=margin_top:字幕渲染在画面顶部安全区,
避开人脸/近景主体常出现的画面中央偏下区域,降低遮挡;
- PrimaryColour &HB3FFFFFF:约 70% 透明文字填充,比原先 &H80FFFFFF(约 50%
更通透,减少对场景的遮挡感;
- OutlineColour &H80000000:半透明黑描边(不再是实心纯黑),
在保留可读性的同时不产生生硬黑框;
- 左右眼水平边距一致(LeftEye/RightEye 各占左右半幅)⇒ 水平相对位置相同,
即零视差(A-1):字幕被渲染在屏幕平面,不产生额外景深冲突。
"""
width, height = resolution.lower().split("x", 1)
# 左眼样式占左半边,右眼样式占右半边,各留 50px 内边距。
left_margin = 50
right_margin = 50
return f"""[Script Info]
Title: VR Dual-Eye Subtitle
ScriptType: v4.00+
Collisions: Normal
PlayResX: {width}
PlayResY: {height}
WrapStyle: 1
ScaledBorderAndShadow: yes
[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: LeftEye,Arial,50,&HB3FFFFFF,&H000000FF,&H80000000,&H80000000,0,0,0,0,50,100,0,0,1,4,0,8,{left_margin},{int(width) // 2},{margin_top},1
Style: RightEye,Arial,50,&HB3FFFFFF,&H000000FF,&H80000000,&H80000000,0,0,0,0,50,100,0,0,1,4,0,8,{int(width) // 2},{right_margin},{margin_top},1
[Events]
Format: Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text
"""
def parse_srt(text: str) -> list[tuple[str, str, str]]:
"""把 SRT 文本解析为 (开始时间, 结束时间, 文本) 条目列表。"""
entries: list[tuple[str, str, str]] = []
lines = text.splitlines()
index = 0
while index < len(lines):
# 跳过序号前的空行,兼容文件开头有换行的情况。
if not lines[index].strip():
index += 1
continue
# 跳过序号行,直接读取下一行时间轴。
index += 1
if index >= len(lines):
break
time_line = lines[index].strip()
index += 1
# 时间轴必须包含分隔符,否则按畸形输入跳过。
if " --> " not in time_line:
continue
# SRT 使用逗号毫秒,ASS 使用点号,需要转换。
start, end = [part.replace(",", ".") for part in time_line.split(" --> ")]
# 连续读取非空行作为字幕文本,多行用 ASS 换行符 \N 连接。
text_lines: list[str] = []
while index < len(lines) and lines[index].strip():
text_lines.append(lines[index])
index += 1
entries.append((start, end, r"\N".join(text_lines)))
index += 1
return entries
def write_ass(
entries: list[tuple[str, str, str]], output_path: Path, resolution: str, margin_top: int = 120
) -> None:
"""把解析后的条目写入 ASS 文件,每个条目输出左右眼两行 Dialogue。
margin_top 控制字幕距画面顶部的安全边距(默认 120),顶部对齐(\an8
使字幕整体落在顶部安全区。左右眼使用相同文本与水平相对位置(零视差,A-1)。"""
lines = [_ass_header(resolution, margin_top=margin_top)]
for start, end, text in entries:
# an8 对齐到屏幕顶部,配合 MarginV 形成顶部安全区,避开中央人脸区域。
lines.append(f"Dialogue: 0,{start},{end},LeftEye,,0,0,0,,{{\\an8}}{text}")
lines.append(f"Dialogue: 0,{start},{end},RightEye,,0,0,0,,{{\\an8}}{text}")
output_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
def invoke(request: InvokeRequest) -> InvokeResponse:
"""把 cn_srt_uri 指向的 SRT 转为 dual_eye.ass 产物。"""
srt_uri = request.inputs.get("cn_srt_uri")
if not srt_uri:
return InvokeResponse(status="failed", error="cn_srt_uri is required")
srt_path = Path(srt_uri)
if not srt_path.is_file():
return InvokeResponse(status="failed", error="srt file not found")
entries = parse_srt(srt_path.read_text(encoding="utf-8"))
output_dir = Path(request.output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / "dual_eye.ass"
# 分辨率默认 3840x1920,覆盖常见 VR 视频尺寸。
resolution = str(request.params.get("resolution", "3840x1920"))
# margin_top 可选:顶部安全边距,不同分辨率/内容可用工作流参数微调。
margin_top = int(request.params.get("margin_top", 120))
write_ass(entries, output_path, resolution, margin_top=margin_top)
return InvokeResponse(status="completed", outputs={"ass_uri": str(output_path)})