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/调查报告:双目视觉景深原理、字幕深度配置公式与遮挡解决方案
This commit is contained in:
2026-09-05 21:03:55 +08:00
parent 5c12bbcb74
commit 2e8bbb15a4
4 changed files with 482 additions and 14 deletions
+30 -13
View File
@@ -11,8 +11,19 @@ from pathlib import Path
from wov_sdk.models import InvokeRequest, InvokeResponse
def _ass_header(resolution: str) -> str:
"""生成 ASS 文件头:脚本信息、左右眼样式和事件格式。"""
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
@@ -28,8 +39,8 @@ 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,&H80FFFFFF,&H000000FF,&H00000000,&H80000000,0,0,0,0,50,100,0,0,1,4,0,2,{left_margin},{int(width) // 2},{int(height) // 2 + 60},1
Style: RightEye,Arial,50,&H80FFFFFF,&H000000FF,&H00000000,&H80000000,0,0,0,0,50,100,0,0,1,4,0,2,{int(width) // 2},{right_margin},{int(height) // 2 + 60},1
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
@@ -67,15 +78,19 @@ def parse_srt(text: str) -> list[tuple[str, str, str]]:
return entries
def write_ass(entries: list[tuple[str, str, str]], output_path: Path, resolution: str) -> None:
"""把解析后的条目写入 ASS 文件,每个条目输出左右眼两行 Dialogue。"""
lines = [_ass_header(resolution)]
for start, end, text in entries:
# an2 对齐到屏幕中央偏下,保证双眼字幕视线自然。
lines.append(f"Dialogue: 0,{start},{end},LeftEye,,0,0,0,,{{\\an2}}{text}")
lines.append(f"Dialogue: 0,{start},{end},RightEye,,0,0,0,,{{\\an2}}{text}")
output_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
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 产物。"""
@@ -93,6 +108,8 @@ def invoke(request: InvokeRequest) -> InvokeResponse:
output_path = output_dir / "dual_eye.ass"
# 分辨率默认 3840x1920,覆盖常见 VR 视频尺寸。
resolution = str(request.params.get("resolution", "3840x1920"))
write_ass(entries, output_path, resolution)
# 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)})