mirror of
https://github.com/AstrBotDevs/AstrBot.git
synced 2026-08-30 17:33:24 +08:00
feat: improve default and local t2i rendering (#9803)
* feat: redesign default t2i template * feat: improve local t2i rendering and migrate defaults * fix: install CJK fonts in Docker image * fix: preserve soft breaks and fit display math
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import pytest
|
||||
|
||||
from astrbot.core.utils.t2i.local_strategy import (
|
||||
CodeBlock,
|
||||
FontManager,
|
||||
HeadingBlock,
|
||||
MarkdownParser,
|
||||
MarkdownRenderer,
|
||||
MathBlock,
|
||||
TableBlock,
|
||||
TextMeasurer,
|
||||
)
|
||||
|
||||
|
||||
def test_text_measurer_uses_content_and_wraps_to_requested_width() -> None:
|
||||
"""Verify measurement uses real text and wrapping never exceeds the limit."""
|
||||
font = FontManager.get_font(24)
|
||||
|
||||
assert (
|
||||
TextMeasurer.get_text_size("WWWW", font)[0]
|
||||
> TextMeasurer.get_text_size("iiii", font)[0]
|
||||
)
|
||||
|
||||
max_width = 180
|
||||
lines = TextMeasurer.split_text_to_fit_width(
|
||||
"这是一段需要自动换行的中文 mixed-with-a-very-long-English-token 内容。",
|
||||
font,
|
||||
max_width,
|
||||
)
|
||||
|
||||
assert len(lines) > 1
|
||||
assert all(TextMeasurer.get_text_size(line, font)[0] <= max_width for line in lines)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_markdown_parser_recognizes_common_rich_blocks() -> None:
|
||||
"""Verify headings, tables, code, and display math receive native blocks."""
|
||||
markdown = """# Heading
|
||||
|
||||
| Name | Value |
|
||||
| :--- | ---: |
|
||||
| Wrap | A long table value |
|
||||
|
||||
```python
|
||||
print("hello")
|
||||
```
|
||||
|
||||
$$
|
||||
E = mc^2
|
||||
$$
|
||||
"""
|
||||
|
||||
blocks = await MarkdownParser.parse(markdown)
|
||||
|
||||
assert any(isinstance(block, HeadingBlock) for block in blocks)
|
||||
assert any(isinstance(block, TableBlock) for block in blocks)
|
||||
assert any(isinstance(block, CodeBlock) for block in blocks)
|
||||
assert any(isinstance(block, MathBlock) for block in blocks)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_markdown_renderer_produces_requested_width_with_wrapped_content() -> (
|
||||
None
|
||||
):
|
||||
"""Verify a narrow render completes without allowing long content to expand it."""
|
||||
renderer = MarkdownRenderer(font_size=22, width=420)
|
||||
markdown = """## 自动换行
|
||||
|
||||
正文包含 **粗体**、`inline_code()` 和一个不会自然断开的超长字符串:abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz。
|
||||
|
||||
```python
|
||||
result = "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz"
|
||||
```
|
||||
|
||||
| 项目 | 说明 |
|
||||
| --- | --- |
|
||||
| 表格 | 这一格也需要在固定宽度里自动换行 |
|
||||
"""
|
||||
|
||||
image = await renderer.render(markdown)
|
||||
|
||||
assert image.width == 420
|
||||
assert image.height > 400
|
||||
@@ -0,0 +1,94 @@
|
||||
import hashlib
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from astrbot.core.utils.t2i import template_manager
|
||||
|
||||
LEGACY_TEMPLATE = "<html>\n<body>legacy default</body>\n</html>\n"
|
||||
CURRENT_TEMPLATE = "<html>\n<body>current default</body>\n</html>\n"
|
||||
CUSTOM_TEMPLATE = "<html>\n<body>customized by user</body>\n</html>\n"
|
||||
|
||||
|
||||
def test_default_template_preserves_soft_breaks_and_fits_display_math() -> None:
|
||||
"""Verify the default template keeps Markdown and wide-math layout safe."""
|
||||
template_path = (
|
||||
Path(__file__).parents[1]
|
||||
/ "astrbot/core/utils/t2i/template/base.html"
|
||||
)
|
||||
template = template_path.read_text(encoding="utf-8")
|
||||
|
||||
paragraph_rule = re.search(r"\n p \{(?P<body>.*?)\n \}", template, re.DOTALL)
|
||||
math_rule = re.search(
|
||||
r"\n \.katex-display \{(?P<body>.*?)\n \}",
|
||||
template,
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
assert paragraph_rule is not None
|
||||
assert "white-space: normal;" in paragraph_rule.group("body")
|
||||
assert math_rule is not None
|
||||
assert "overflow-x: auto;" in math_rule.group("body")
|
||||
assert 'querySelectorAll(".katex-display")' in template
|
||||
assert "renderedWidth > availableWidth" in template
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("user_content", "expected_content"),
|
||||
[
|
||||
pytest.param(None, CURRENT_TEMPLATE, id="missing-template"),
|
||||
pytest.param(LEGACY_TEMPLATE, CURRENT_TEMPLATE, id="legacy-template"),
|
||||
pytest.param(
|
||||
LEGACY_TEMPLATE.replace("\n", "\r\n"),
|
||||
CURRENT_TEMPLATE,
|
||||
id="legacy-template-crlf",
|
||||
),
|
||||
pytest.param(CUSTOM_TEMPLATE, CUSTOM_TEMPLATE, id="custom-template"),
|
||||
],
|
||||
)
|
||||
def test_initialize_user_templates_migrates_only_unmodified_defaults(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
user_content: str | None,
|
||||
expected_content: str,
|
||||
) -> None:
|
||||
"""Verify automatic migration preserves customized user templates.
|
||||
|
||||
Args:
|
||||
monkeypatch: Pytest fixture used to isolate AstrBot paths and legacy hashes.
|
||||
tmp_path: Temporary directory used for built-in and user templates.
|
||||
user_content: Existing user template content, or None when it is missing.
|
||||
expected_content: Template content expected after manager initialization.
|
||||
"""
|
||||
builtin_root = tmp_path / "astrbot-root"
|
||||
builtin_dir = builtin_root / "astrbot/core/utils/t2i/template"
|
||||
builtin_dir.mkdir(parents=True)
|
||||
(builtin_dir / "base.html").write_text(CURRENT_TEMPLATE, encoding="utf-8")
|
||||
|
||||
data_root = tmp_path / "data"
|
||||
user_dir = data_root / "t2i_templates"
|
||||
if user_content is not None:
|
||||
user_dir.mkdir(parents=True)
|
||||
(user_dir / "base.html").write_text(user_content, encoding="utf-8")
|
||||
|
||||
legacy_hash = hashlib.sha256(LEGACY_TEMPLATE.encode()).hexdigest()
|
||||
monkeypatch.setattr(
|
||||
template_manager,
|
||||
"_LEGACY_CORE_TEMPLATE_HASHES",
|
||||
{"base.html": frozenset({legacy_hash})},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
template_manager,
|
||||
"get_astrbot_path",
|
||||
lambda: str(builtin_root),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
template_manager,
|
||||
"get_astrbot_data_path",
|
||||
lambda: str(data_root),
|
||||
)
|
||||
|
||||
template_manager.TemplateManager()
|
||||
|
||||
assert (user_dir / "base.html").read_text(encoding="utf-8") == expected_content
|
||||
Reference in New Issue
Block a user