mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-07-06 17:31:16 -03:00
feat(agent): render HF README as HTML in modelDescription, move converter to skill-local module
- Add inline convert_readme_to_html() in new skill-local md_to_html.py
(zero external deps, handles h1-h4/bold/italic/code/lists/tables/links/hr)
- Strip YAML frontmatter, <Gallery />, badge images, HTML comments pre-conversion
- Fix indented whitespace after lists being misidentified as code blocks
- Fix HTML double-escaping in _inline_md (each pattern escapes independently)
- LLM short_description → civitai.description ("About this version" sidebar)
- raw README HTML → modelDescription (description tab, always available offline)
- Pass full readme_content from agent_service to post_processor
- 51 tests for converter + 4 updated/added post-processor tests
This commit is contained in:
@@ -315,3 +315,157 @@ class TestRefreshCache:
|
||||
mock_read.return_value = {}
|
||||
result = await refresh_cache("/some/path.safetensors")
|
||||
assert result is False
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# convert_readme_to_html — pure function, no mocks needed
|
||||
# ======================================================================
|
||||
|
||||
|
||||
class TestConvertReadmeToHtml:
|
||||
"""Tests for the inline markdown→HTML converter."""
|
||||
|
||||
def test_empty_input(self):
|
||||
from py.services.agent.skills.enrich_hf_metadata.md_to_html import \
|
||||
convert_readme_to_html
|
||||
|
||||
assert convert_readme_to_html("") == ""
|
||||
assert convert_readme_to_html(None) == "" # type: ignore[arg-type]
|
||||
|
||||
def test_heading(self):
|
||||
from py.services.agent.skills.enrich_hf_metadata.md_to_html import \
|
||||
convert_readme_to_html
|
||||
|
||||
result = convert_readme_to_html("# Title")
|
||||
assert "<h1>" in result and "Title" in result
|
||||
|
||||
def test_subheadings(self):
|
||||
from py.services.agent.skills.enrich_hf_metadata.md_to_html import \
|
||||
convert_readme_to_html
|
||||
|
||||
md = "## Overview\n\n### Details"
|
||||
result = convert_readme_to_html(md)
|
||||
assert "<h2>Overview</h2>" in result
|
||||
assert "<h3>Details</h3>" in result
|
||||
|
||||
def test_bold_and_italic(self):
|
||||
from py.services.agent.skills.enrich_hf_metadata.md_to_html import \
|
||||
convert_readme_to_html
|
||||
|
||||
md = "**bold** and *italic*"
|
||||
result = convert_readme_to_html(md)
|
||||
assert "<strong>bold</strong>" in result
|
||||
assert "<em>italic</em>" in result
|
||||
|
||||
def test_inline_code(self):
|
||||
from py.services.agent.skills.enrich_hf_metadata.md_to_html import \
|
||||
convert_readme_to_html
|
||||
|
||||
md = "Use `model.train()`"
|
||||
result = convert_readme_to_html(md)
|
||||
assert "<code>" in result and "model.train()" in result
|
||||
|
||||
def test_fenced_code_block(self):
|
||||
from py.services.agent.skills.enrich_hf_metadata.md_to_html import \
|
||||
convert_readme_to_html
|
||||
|
||||
md = "```python\nprint('hello')\n```"
|
||||
result = convert_readme_to_html(md)
|
||||
assert "<pre>" in result
|
||||
assert "print" in result and "hello" in result
|
||||
|
||||
def test_unordered_list(self):
|
||||
from py.services.agent.skills.enrich_hf_metadata.md_to_html import \
|
||||
convert_readme_to_html
|
||||
|
||||
md = "- item one\n- item two"
|
||||
result = convert_readme_to_html(md)
|
||||
assert "<ul>" in result
|
||||
assert "<li>item one</li>" in result
|
||||
assert "<li>item two</li>" in result
|
||||
|
||||
def test_ordered_list(self):
|
||||
from py.services.agent.skills.enrich_hf_metadata.md_to_html import \
|
||||
convert_readme_to_html
|
||||
|
||||
md = "1. first\n2. second"
|
||||
result = convert_readme_to_html(md)
|
||||
assert "<ol>" in result
|
||||
assert "<li>first</li>" in result
|
||||
assert "<li>second</li>" in result
|
||||
|
||||
def test_link(self):
|
||||
from py.services.agent.skills.enrich_hf_metadata.md_to_html import \
|
||||
convert_readme_to_html
|
||||
|
||||
md = "[click here](https://example.com)"
|
||||
result = convert_readme_to_html(md)
|
||||
assert '<a href="https://example.com">click here</a>' in result
|
||||
|
||||
def test_badge_image_stripped(self):
|
||||
from py.services.agent.skills.enrich_hf_metadata.md_to_html import \
|
||||
convert_readme_to_html
|
||||
|
||||
md = ""
|
||||
result = convert_readme_to_html(md)
|
||||
assert "img.shields.io" not in result
|
||||
|
||||
def test_gallery_stripped(self):
|
||||
from py.services.agent.skills.enrich_hf_metadata.md_to_html import \
|
||||
convert_readme_to_html
|
||||
|
||||
md = "Some text\n<Gallery />\nmore text"
|
||||
result = convert_readme_to_html(md)
|
||||
assert "<Gallery" not in result
|
||||
|
||||
def test_yaml_frontmatter_stripped(self):
|
||||
from py.services.agent.skills.enrich_hf_metadata.md_to_html import \
|
||||
convert_readme_to_html
|
||||
|
||||
md = "---\ntags:\n - lora\nbase_model: flux\n---\n\n# Real content"
|
||||
result = convert_readme_to_html(md)
|
||||
assert "base_model" not in result
|
||||
assert "<h1>Real content</h1>" in result
|
||||
|
||||
def test_table(self):
|
||||
from py.services.agent.skills.enrich_hf_metadata.md_to_html import \
|
||||
convert_readme_to_html
|
||||
|
||||
md = "| A | B |\n|---|---|\n| 1 | 2 |"
|
||||
result = convert_readme_to_html(md)
|
||||
assert "<table>" in result
|
||||
assert "<th>A</th>" in result
|
||||
assert "<td>1</td>" in result
|
||||
|
||||
def test_horizontal_rule(self):
|
||||
from py.services.agent.skills.enrich_hf_metadata.md_to_html import \
|
||||
convert_readme_to_html
|
||||
|
||||
md = "before\n\n---\n\nafter"
|
||||
result = convert_readme_to_html(md)
|
||||
assert "<hr>" in result
|
||||
|
||||
def test_inline_code_preserves_angle_bracket(self):
|
||||
from py.services.agent.skills.enrich_hf_metadata.md_to_html import \
|
||||
convert_readme_to_html
|
||||
|
||||
result = convert_readme_to_html("Use `a < b` in code")
|
||||
assert "<code>a < b</code>" in result
|
||||
|
||||
def test_blockquote(self):
|
||||
from py.services.agent.skills.enrich_hf_metadata.md_to_html import \
|
||||
convert_readme_to_html
|
||||
|
||||
md = "> quoted text"
|
||||
result = convert_readme_to_html(md)
|
||||
assert "<blockquote>" in result
|
||||
assert "quoted text" in result
|
||||
|
||||
def test_indented_whitespace_not_treated_as_code(self):
|
||||
from py.services.agent.skills.enrich_hf_metadata.md_to_html import \
|
||||
convert_readme_to_html
|
||||
|
||||
md = "- item\n \n## heading after spacing"
|
||||
result = convert_readme_to_html(md)
|
||||
assert "<pre>" not in result
|
||||
assert "<h2>heading after spacing</h2>" in result
|
||||
|
||||
@@ -67,7 +67,7 @@ class TestEnrichHfMetadata:
|
||||
MIN_LLM_OUTPUT = {
|
||||
"base_model": "",
|
||||
"trigger_words": [],
|
||||
"description": "",
|
||||
"short_description": "",
|
||||
"tags": [],
|
||||
"preview_url": "",
|
||||
"confidence": "low",
|
||||
@@ -167,23 +167,82 @@ class TestEnrichHfMetadata:
|
||||
applied = mock_apply.call_args[0][1]
|
||||
assert applied["civitai"]["trainedWords"] == ["trigger1", "trigger2"]
|
||||
|
||||
# -- description -----------------------------------------------------
|
||||
# -- short_description → civitai.description -------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_description_set_when_empty(self, processor):
|
||||
llm = {**self.MIN_LLM_OUTPUT, "description": "A model description"}
|
||||
async def test_short_description_written_to_civitai(self, processor):
|
||||
"""short_description written to civitai.description for HF models."""
|
||||
llm = {**self.MIN_LLM_OUTPUT, "short_description": "A short summary"}
|
||||
with (
|
||||
mock.patch("py.agent_cli.apply_metadata_updates") as mock_apply,
|
||||
mock.patch("py.agent_cli.download_preview", return_value=False),
|
||||
mock.patch("py.agent_cli.download_preview", return_value=None),
|
||||
mock.patch("py.agent_cli.refresh_cache"),
|
||||
):
|
||||
await processor.process(
|
||||
skill_name="enrich_hf_metadata",
|
||||
model_path="/p.safetensors",
|
||||
llm_output=llm,
|
||||
metadata={"modelDescription": ""},
|
||||
metadata={"from_civitai": False},
|
||||
)
|
||||
assert "modelDescription" in mock_apply.call_args[0][1]
|
||||
applied = mock_apply.call_args[0][1]
|
||||
assert applied["civitai"]["description"] == "A short summary"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_short_description_skipped_for_civitai_model(self, processor):
|
||||
"""short_description NOT written for CivitAI models (has own description)."""
|
||||
llm = {**self.MIN_LLM_OUTPUT, "short_description": "A short summary"}
|
||||
with (
|
||||
mock.patch("py.agent_cli.apply_metadata_updates") as mock_apply,
|
||||
mock.patch("py.agent_cli.download_preview", return_value=None),
|
||||
mock.patch("py.agent_cli.refresh_cache"),
|
||||
):
|
||||
await processor.process(
|
||||
skill_name="enrich_hf_metadata",
|
||||
model_path="/p.safetensors",
|
||||
llm_output=llm,
|
||||
metadata={"from_civitai": True},
|
||||
)
|
||||
applied = mock_apply.call_args[0][1]
|
||||
assert "civitai" not in applied or "description" not in applied.get("civitai", {})
|
||||
|
||||
# -- readme_content → modelDescription -------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readme_content_converted_to_model_description(self, processor):
|
||||
"""Raw README converted to HTML and stored as modelDescription."""
|
||||
with (
|
||||
mock.patch("py.agent_cli.apply_metadata_updates") as mock_apply,
|
||||
mock.patch("py.agent_cli.download_preview", return_value=None),
|
||||
mock.patch("py.agent_cli.refresh_cache"),
|
||||
):
|
||||
await processor.process(
|
||||
skill_name="enrich_hf_metadata",
|
||||
model_path="/p.safetensors",
|
||||
llm_output=self.MIN_LLM_OUTPUT,
|
||||
metadata={"from_civitai": False},
|
||||
readme_content="# Hello\n\nThis is **bold**.",
|
||||
)
|
||||
applied = mock_apply.call_args[0][1]
|
||||
assert "<h1>Hello</h1>" in applied.get("modelDescription", "")
|
||||
assert "<strong>bold</strong>" in applied.get("modelDescription", "")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readme_content_skipped_for_civitai_model(self, processor):
|
||||
"""README content NOT converted for CivitAI models."""
|
||||
with (
|
||||
mock.patch("py.agent_cli.apply_metadata_updates") as mock_apply,
|
||||
mock.patch("py.agent_cli.download_preview", return_value=None),
|
||||
mock.patch("py.agent_cli.refresh_cache"),
|
||||
):
|
||||
await processor.process(
|
||||
skill_name="enrich_hf_metadata",
|
||||
model_path="/p.safetensors",
|
||||
llm_output=self.MIN_LLM_OUTPUT,
|
||||
metadata={"from_civitai": True},
|
||||
readme_content="# Hello",
|
||||
)
|
||||
applied = mock_apply.call_args[0][1]
|
||||
assert "modelDescription" not in applied
|
||||
|
||||
# -- tags ------------------------------------------------------------
|
||||
|
||||
|
||||
Reference in New Issue
Block a user