fix: 标题默认位置 fallback 改为 bottom,与前端 DEFAULT_TITLE_SETTINGS 对齐 #1691

Merged
xiaoxia merged 1 commits from fix/title-position-fallback-wrap-consistency into develop 2026-09-04 15:19:37 +08:00
6 changed files with 69 additions and 27 deletions
@@ -213,7 +213,7 @@ def generate_ass_from_timeline(
t_shadow.get("offset_x", 2) if t_shadow.get("enabled", False) else 0,
t_shadow.get("offset_y", 2) if t_shadow.get("enabled", False) else 0,
)
t_alignment = position_to_ass_alignment(title_cfg.get("position", "top"))
t_alignment = position_to_ass_alignment(title_cfg.get("position", "bottom"))
title_style_line = build_ass_style(
"TitleStyle",
+4 -5
View File
@@ -81,14 +81,14 @@ def position_to_ass_alignment(position: str) -> int:
position: 位置字符串 top/center/bottom
Returns:
ASS 对齐编号,默认 8(顶部居中
ASS 对齐编号,默认 2(底部居中,与前端 DEFAULT_TITLE_SETTINGS.position="bottom" 对齐
"""
mapping = {
"top": 8,
"center": 5,
"bottom": 2,
}
return mapping.get(position, 8)
return mapping.get(position, 2)
# ── Style 行构建 ──────────────────────────────────────────────────────────────
@@ -226,7 +226,6 @@ def _wrap_title_text(
# 换行计算使用原始 font_size,与 CSS 预览一致;1.35x 补偿仅用于 ASS Fontsize 渲染
# 先按已有 \N 分段,每段独立自动换行,最后用 \N 拼回
segments = text.split("\\N")
wrapped_segments: list[str] = []
@@ -386,8 +385,8 @@ def build_ass_content(
# position → alignment 三档逻辑,现有输出保持一字节不变。
title_pos = _parse_title_position(title_config, video_width, video_height)
title_alignment = 5 if title_pos is not None else position_to_ass_alignment(
title_config.get("position", "top")
title_alignment = (
5 if title_pos is not None else position_to_ass_alignment(title_config.get("position", "bottom"))
)
styles.append(
@@ -83,11 +83,11 @@ class TestPositionToAssAlignment:
def test_bottom(self):
assert position_to_ass_alignment("bottom") == 2
def test_unknown_defaults_top(self):
assert position_to_ass_alignment("unknown") == 8
def test_unknown_defaults_bottom(self):
assert position_to_ass_alignment("unknown") == 2
def test_empty_defaults_top(self):
assert position_to_ass_alignment("") == 8
def test_empty_defaults_bottom(self):
assert position_to_ass_alignment("") == 2
# ============================================================
@@ -581,6 +581,7 @@ class TestConstants:
assert isinstance(TITLE_MARGIN_BOTTOM, int)
assert isinstance(TITLE_MARGIN_SIDE, int)
# ============================================================
# _wrap_title_text 换行逻辑验证
# ============================================================
+46 -4
View File
@@ -65,11 +65,11 @@ class TestPositionToAssAlignment:
def test_bottom(self):
assert position_to_ass_alignment("bottom") == 2
def test_unknown_default_top(self):
assert position_to_ass_alignment("unknown") == 8
def test_unknown_default_bottom(self):
assert position_to_ass_alignment("unknown") == 2
def test_empty_default_top(self):
assert position_to_ass_alignment("") == 8
def test_empty_default_bottom(self):
assert position_to_ass_alignment("") == 2
# ── Style 行构建 ─────────────────────────────────────────────────────────────
@@ -747,3 +747,45 @@ class TestTitleFreePosition:
line for line in content.splitlines() if line.startswith("Dialogue:") and "SubtitleStyle" in line
][0]
assert "\\pos(" not in sub_dialogue
class TestDefaultPositionBottom:
"""默认 position 应为 bottomalignment=2),与前端 DEFAULT_TITLE_SETTINGS 对齐。"""
def _base_kwargs(self):
return dict(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="测试标题",
)
def test_no_position_defaults_to_bottom_alignment(self):
"""不传 position 时,Alignment 应为 2bottom)。"""
content = build_ass_content(
**self._base_kwargs(),
title_config={"size": 36},
)
style_line = [line for line in content.splitlines() if line.startswith("Style: TitleStyle")][0]
fields = [f.strip() for f in style_line.split(",")]
assert fields[18] == "2", f"Expected alignment 2 (bottom), got {fields[18]}"
def test_no_position_no_coords_defaults_to_bottom(self):
"""不传 position 也不传坐标时,走 bottom 三档逻辑。"""
content = build_ass_content(
**self._base_kwargs(),
title_config={},
)
style_line = [line for line in content.splitlines() if line.startswith("Style: TitleStyle")][0]
fields = [f.strip() for f in style_line.split(",")]
assert fields[18] == "2"
def test_explicit_top_still_works(self):
"""显式传 position='top' 仍然得到 alignment=8。"""
content = build_ass_content(
**self._base_kwargs(),
title_config={"position": "top", "size": 36},
)
style_line = [line for line in content.splitlines() if line.startswith("Style: TitleStyle")][0]
fields = [f.strip() for f in style_line.split(",")]
assert fields[18] == "8"
+5 -5
View File
@@ -67,11 +67,11 @@ class TestPositionToAssAlignment:
def test_bottom(self):
assert _position_to_ass_alignment("bottom") == 2
def test_unknown_returns_top_default(self):
assert _position_to_ass_alignment("unknown") == 8
assert _position_to_ass_alignment("") == 8
assert _position_to_ass_alignment("left") == 8
assert _position_to_ass_alignment(None) == 8
def test_unknown_returns_bottom_default(self):
assert _position_to_ass_alignment("unknown") == 2
assert _position_to_ass_alignment("") == 2
assert _position_to_ass_alignment("left") == 2
assert _position_to_ass_alignment(None) == 2
class TestBuildAssStyle:
+8 -8
View File
@@ -66,15 +66,15 @@ class TestPositionToAssAlignment:
"""center → 居中(5)."""
assert _position_to_ass_alignment("center") == 5
def test_unknown_defaults_to_top(self):
"""未知位置默认部(8)."""
assert _position_to_ass_alignment("unknown") == 8
assert _position_to_ass_alignment("top_left") == 8
assert _position_to_ass_alignment("bottom_right") == 8
def test_unknown_defaults_to_bottom(self):
"""未知位置默认部(2)."""
assert _position_to_ass_alignment("unknown") == 2
assert _position_to_ass_alignment("top_left") == 2
assert _position_to_ass_alignment("bottom_right") == 2
def test_empty_string_defaults_to_top(self):
"""空字符串默认部."""
assert _position_to_ass_alignment("") == 8
def test_empty_string_defaults_to_bottom(self):
"""空字符串默认部."""
assert _position_to_ass_alignment("") == 2
class TestBuildAssStyle: