feat: VR字幕默认MarginV改为700并统一历史字幕样式

- srt-to-dual-eye-ass: 默认顶部安全边距 margin_top 120→700(2026-09),
  120落在画面最顶需抬头看,700为实测合适值、视线自然可读
- nodes/ass.py: 样式定义抽为单一事实来源(DEFAULT_MARGIN_TOP/颜色常量
  +ass_header/style_row/dialogue_line),新生成字幕与历史统一脚本共用同一
  出口,以后调样式只改一处、两边永不漂移
- 新增scripts/unify_ass_style.py: 解析旧文件分辨率与全部Dialogue后经
  ass_header/dialogue_line重建,把媒体库历史样式(底部an2实心白1200/
  半透明1020/顶部120)原地统一为an8顶部对齐+70%透明+MarginV=700;
  默认dry-run预览,--apply写盘;非VR字幕自动跳过
- /mnt/fnOS/123 库401个.CN_dual_eye.ass已全部改写(0失败),269764条
  字幕事件无an2残留,二次运行幂等
- 测试: ass默认值断言120→700,新增invoke默认700用例;新增
  tests/test_unify_ass_style.py(历史三世代收敛/与write_ass逐字节一致/
  幂等/CLI dry-run与apply)
This commit is contained in:
2026-09-06 18:02:31 +08:00
parent 44a607b636
commit 4d2a1912da
5 changed files with 614 additions and 45 deletions
+27 -4
View File
@@ -678,20 +678,21 @@ def test_ass_parse_and_write(tmp_path) -> None:
def test_ass_top_aligned_and_translucent(tmp_path) -> None:
"""验证字幕默认渲染到顶部安全区且文字/描边带透明度。
B-1:对齐 an8(顶部居中),MarginV 取顶部安全边距默认 120
B-1:对齐 an8(顶部居中),MarginV 取顶部安全边距默认 7002026-09 起,
字幕置于视线自然可读位置;旧默认 120 落在画面最顶需抬头观看);
透明度:文字填充 &HB3FFFFFF(约 70% 透明),描边 &H80000000(半透明黑),
而左右眼水平相对位置一致保持零视差(A-1,字幕在屏幕平面)。"""
entries = parse_srt(SAMPLE_SRT)
output = tmp_path / "out.ass"
write_ass(entries, output, "3840x1920")
content = output.read_text(encoding="utf-8")
# 顶部对齐 an8、顶部安全边距默认 120。
# 顶部对齐 an8、顶部安全边距默认 700。
assert r"{\an8}" in content
assert r"{\an2}" not in content
# 样式行:MarginV=120(顶部安全区),填充 &HB3FFFFFF,描边 &H80000000。
# 样式行:MarginV=700(顶部安全区),填充 &HB3FFFFFF,描边 &H80000000。
assert "&HB3FFFFFF" in content
assert "&H80000000" in content
assert ",0,0,0,0,50,100,0,0,1,4,0,8,50,1920,120,1" in content
assert ",0,0,0,0,50,100,0,0,1,4,0,8,50,1920,700,1" in content
# 左右眼两行文本一致(水平相对位置相同 → 零视差/A-1)。
assert content.count(r"第一行\N第二行") == 2
@@ -725,6 +726,28 @@ def test_ass_invoke_reads_margin_top(tmp_path) -> None:
assert ",0,0,0,0,50,100,0,0,1,4,0,8,50,960,90,1" in content
def test_ass_invoke_default_margin_top_is_700(tmp_path) -> None:
"""验证 invoke 未显式传 margin_top 时默认生成 MarginV=700 的样式。
保证之后生成的字幕默认都落在顶部安全区下方(2026-09 起),
不再回退到旧的 120(需抬头观看)。"""
source = tmp_path / "in.srt"
source.write_text(SAMPLE_SRT, encoding="utf-8")
response = ass_invoke(
InvokeRequest(
run_id="run_def700",
node_instance_id="ni_def700",
inputs={"cn_srt_uri": str(source)},
params={"resolution": "1920x1080"},
output_dir=str(tmp_path / "out"),
)
)
assert response.status == "completed"
content = Path(response.outputs["ass_uri"]).read_text(encoding="utf-8")
assert "an8" in content
assert ",0,0,0,0,50,100,0,0,1,4,0,8,50,960,700,1" in content
def test_ass_parse_malformed(tmp_path) -> None:
"""验证畸形 SRT 不会抛出异常且返回空条目或忽略坏行。"""
source = tmp_path / "bad.srt"