Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 15e429ffb1 |
@@ -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_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_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(
|
title_style_line = build_ass_style(
|
||||||
"TitleStyle",
|
"TitleStyle",
|
||||||
|
|||||||
@@ -81,14 +81,14 @@ def position_to_ass_alignment(position: str) -> int:
|
|||||||
position: 位置字符串 top/center/bottom
|
position: 位置字符串 top/center/bottom
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ASS 对齐编号,默认 8(顶部居中)
|
ASS 对齐编号,默认 2(底部居中,与前端 DEFAULT_TITLE_SETTINGS.position="bottom" 对齐)
|
||||||
"""
|
"""
|
||||||
mapping = {
|
mapping = {
|
||||||
"top": 8,
|
"top": 8,
|
||||||
"center": 5,
|
"center": 5,
|
||||||
"bottom": 2,
|
"bottom": 2,
|
||||||
}
|
}
|
||||||
return mapping.get(position, 8)
|
return mapping.get(position, 2)
|
||||||
|
|
||||||
|
|
||||||
# ── Style 行构建 ──────────────────────────────────────────────────────────────
|
# ── Style 行构建 ──────────────────────────────────────────────────────────────
|
||||||
@@ -226,7 +226,6 @@ def _wrap_title_text(
|
|||||||
|
|
||||||
# 换行计算使用原始 font_size,与 CSS 预览一致;1.35x 补偿仅用于 ASS Fontsize 渲染
|
# 换行计算使用原始 font_size,与 CSS 预览一致;1.35x 补偿仅用于 ASS Fontsize 渲染
|
||||||
|
|
||||||
|
|
||||||
# 先按已有 \N 分段,每段独立自动换行,最后用 \N 拼回
|
# 先按已有 \N 分段,每段独立自动换行,最后用 \N 拼回
|
||||||
segments = text.split("\\N")
|
segments = text.split("\\N")
|
||||||
wrapped_segments: list[str] = []
|
wrapped_segments: list[str] = []
|
||||||
@@ -386,8 +385,8 @@ def build_ass_content(
|
|||||||
# position → alignment 三档逻辑,现有输出保持一字节不变。
|
# position → alignment 三档逻辑,现有输出保持一字节不变。
|
||||||
title_pos = _parse_title_position(title_config, video_width, video_height)
|
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_alignment = (
|
||||||
title_config.get("position", "top")
|
5 if title_pos is not None else position_to_ass_alignment(title_config.get("position", "bottom"))
|
||||||
)
|
)
|
||||||
|
|
||||||
styles.append(
|
styles.append(
|
||||||
|
|||||||
@@ -83,11 +83,11 @@ class TestPositionToAssAlignment:
|
|||||||
def test_bottom(self):
|
def test_bottom(self):
|
||||||
assert position_to_ass_alignment("bottom") == 2
|
assert position_to_ass_alignment("bottom") == 2
|
||||||
|
|
||||||
def test_unknown_defaults_top(self):
|
def test_unknown_defaults_bottom(self):
|
||||||
assert position_to_ass_alignment("unknown") == 8
|
assert position_to_ass_alignment("unknown") == 2
|
||||||
|
|
||||||
def test_empty_defaults_top(self):
|
def test_empty_defaults_bottom(self):
|
||||||
assert position_to_ass_alignment("") == 8
|
assert position_to_ass_alignment("") == 2
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -581,6 +581,7 @@ class TestConstants:
|
|||||||
assert isinstance(TITLE_MARGIN_BOTTOM, int)
|
assert isinstance(TITLE_MARGIN_BOTTOM, int)
|
||||||
assert isinstance(TITLE_MARGIN_SIDE, int)
|
assert isinstance(TITLE_MARGIN_SIDE, int)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# _wrap_title_text 换行逻辑验证
|
# _wrap_title_text 换行逻辑验证
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
@@ -65,11 +65,11 @@ class TestPositionToAssAlignment:
|
|||||||
def test_bottom(self):
|
def test_bottom(self):
|
||||||
assert position_to_ass_alignment("bottom") == 2
|
assert position_to_ass_alignment("bottom") == 2
|
||||||
|
|
||||||
def test_unknown_default_top(self):
|
def test_unknown_default_bottom(self):
|
||||||
assert position_to_ass_alignment("unknown") == 8
|
assert position_to_ass_alignment("unknown") == 2
|
||||||
|
|
||||||
def test_empty_default_top(self):
|
def test_empty_default_bottom(self):
|
||||||
assert position_to_ass_alignment("") == 8
|
assert position_to_ass_alignment("") == 2
|
||||||
|
|
||||||
|
|
||||||
# ── Style 行构建 ─────────────────────────────────────────────────────────────
|
# ── Style 行构建 ─────────────────────────────────────────────────────────────
|
||||||
@@ -747,3 +747,45 @@ class TestTitleFreePosition:
|
|||||||
line for line in content.splitlines() if line.startswith("Dialogue:") and "SubtitleStyle" in line
|
line for line in content.splitlines() if line.startswith("Dialogue:") and "SubtitleStyle" in line
|
||||||
][0]
|
][0]
|
||||||
assert "\\pos(" not in sub_dialogue
|
assert "\\pos(" not in sub_dialogue
|
||||||
|
|
||||||
|
|
||||||
|
class TestDefaultPositionBottom:
|
||||||
|
"""默认 position 应为 bottom(alignment=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 应为 2(bottom)。"""
|
||||||
|
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"
|
||||||
|
|||||||
@@ -67,11 +67,11 @@ class TestPositionToAssAlignment:
|
|||||||
def test_bottom(self):
|
def test_bottom(self):
|
||||||
assert _position_to_ass_alignment("bottom") == 2
|
assert _position_to_ass_alignment("bottom") == 2
|
||||||
|
|
||||||
def test_unknown_returns_top_default(self):
|
def test_unknown_returns_bottom_default(self):
|
||||||
assert _position_to_ass_alignment("unknown") == 8
|
assert _position_to_ass_alignment("unknown") == 2
|
||||||
assert _position_to_ass_alignment("") == 8
|
assert _position_to_ass_alignment("") == 2
|
||||||
assert _position_to_ass_alignment("left") == 8
|
assert _position_to_ass_alignment("left") == 2
|
||||||
assert _position_to_ass_alignment(None) == 8
|
assert _position_to_ass_alignment(None) == 2
|
||||||
|
|
||||||
|
|
||||||
class TestBuildAssStyle:
|
class TestBuildAssStyle:
|
||||||
|
|||||||
@@ -66,15 +66,15 @@ class TestPositionToAssAlignment:
|
|||||||
"""center → 居中(5)."""
|
"""center → 居中(5)."""
|
||||||
assert _position_to_ass_alignment("center") == 5
|
assert _position_to_ass_alignment("center") == 5
|
||||||
|
|
||||||
def test_unknown_defaults_to_top(self):
|
def test_unknown_defaults_to_bottom(self):
|
||||||
"""未知位置默认顶部(8)."""
|
"""未知位置默认底部(2)."""
|
||||||
assert _position_to_ass_alignment("unknown") == 8
|
assert _position_to_ass_alignment("unknown") == 2
|
||||||
assert _position_to_ass_alignment("top_left") == 8
|
assert _position_to_ass_alignment("top_left") == 2
|
||||||
assert _position_to_ass_alignment("bottom_right") == 8
|
assert _position_to_ass_alignment("bottom_right") == 2
|
||||||
|
|
||||||
def test_empty_string_defaults_to_top(self):
|
def test_empty_string_defaults_to_bottom(self):
|
||||||
"""空字符串默认顶部."""
|
"""空字符串默认底部."""
|
||||||
assert _position_to_ass_alignment("") == 8
|
assert _position_to_ass_alignment("") == 2
|
||||||
|
|
||||||
|
|
||||||
class TestBuildAssStyle:
|
class TestBuildAssStyle:
|
||||||
|
|||||||
Reference in New Issue
Block a user