feat(agent): extract HF widget gallery images into civitai.images with recommended dimensions

- Add extract_gallery_images() to parse YAML widget entries from README
  frontmatter, convert relative image URLs to absolute HF URLs, and
  build civitai.images-compatible entries with prompt metadata
- LLM now extracts recommended_width/recommended_height from README
  (e.g. "Best Dimensions"), used as gallery image dimensions
- extract_gallery_images() accepts default_width/height parameters,
  falling back to 512x512 when LLM provides no recommendation
- Frontend ShowcaseView.js: defensive NaN guard for 0 width/height
- post_processor: consistently merge civitai updates across triggers,
  description, and gallery blocks with distinct variable names
- SKILL.md: add recommended_width/recommended_height to output schema
- 62 tests pass, including gallery extraction and dimension tests
This commit is contained in:
Will Miao
2026-07-03 07:07:19 +08:00
parent 88349bf944
commit ee8250c26c
6 changed files with 314 additions and 9 deletions

View File

@@ -78,6 +78,9 @@ Sources to consider:
Return empty array if no meaningful content tags remain after filtering.
### recommended_width, recommended_height
The recommended image generation resolution for this model, in pixels. Look for sections like "Best Dimensions", "Recommended size", "Suggested resolution", or similar phrasing in the README. Prefer the explicitly marked "Best" or default resolution. If the table/list has multiple entries (e.g. "768 x 1024 (Best)" and "1024 x 1024 (Default)"), use the one marked "Best". Return integers. If no resolution can be determined, return 0 for both.
### preview_url
The URL of the most suitable preview image from the README. Look for image tags (e.g. `![alt](url)`) and the YAML frontmatter `widget:` section (which often has `output.url` fields). Choose the first image that appears to be a generation example (not a logo or diagram). Construct the absolute URL as `https://huggingface.co/{{repo}}/resolve/main/{filename}`. If no suitable image is found, return an empty string.
@@ -98,6 +101,8 @@ Return ONLY a JSON object with exactly these fields (no markdown fences, no extr
"trigger_words": ["<word1>", "<word2>"],
"short_description": "<1-2 sentence summary>",
"tags": ["<tag1>", "<tag2>"],
"recommended_width": 768,
"recommended_height": 1024,
"preview_url": "<image URL or empty string>",
"confidence": "<high|medium|low>"
}

View File

@@ -12,6 +12,120 @@ import re
from typing import List, Tuple
_REPO_URL_PATTERN = re.compile(r"https?://huggingface\.co/([^/]+/[^/]+)")
def extract_repo_from_hf_url(hf_url: str) -> str:
"""Extract ``user/repo`` from a HuggingFace URL."""
m = _REPO_URL_PATTERN.match(hf_url)
return m.group(1) if m else ""
def extract_gallery_images(
markdown_text: str,
repo: str,
default_width: int = 512,
default_height: int = 512,
) -> List[dict]:
"""Extract widget/gallery images from the YAML frontmatter of a HF README.
Args:
markdown_text: Raw README content.
repo: HF repo identifier (``user/repo``).
default_width: Fallback width when the README provides no dimension.
default_height: Fallback height when the README provides no dimension.
Returns a list of dicts compatible with the ``civitai.images`` metadata
format, each containing ``url`` (absolute HF URL), ``meta.prompt``,
``width``, ``height``, and ``type``. Returns an empty list when no
widget entries are found or when *repo* is empty.
"""
if not markdown_text or not repo:
return []
frontmatter = _extract_frontmatter(markdown_text)
if not frontmatter:
return []
images: List[dict] = []
base_url = f"https://huggingface.co/{repo}/resolve/main"
w = default_width or 512
h = default_height or 512
# Find the `widget:` section
widget_match = re.search(r"^widget:\s*$", frontmatter, re.MULTILINE)
if not widget_match:
return images
# Split entries starting with `- text:`
entries = re.split(r"\n- text:", frontmatter[widget_match.end():])
for entry in entries:
if not entry.strip():
continue
entry = entry.strip()
# Extract text (prompt)
text = ""
# Quoted inline: `"some prompt"`
qm = re.match(r'^"((?:[^"\\]|\\.)*)"', entry)
if qm:
text = qm.group(1)
else:
# Multi-line YAML scalar: `>-\n line1\n line2`
mm = re.match(r"^>(?:-\s*)?\n((?:.+(?:\n|$))+)", entry, re.MULTILINE)
if mm:
raw = mm.group(1)
# Take lines until a line starts with a YAML key (word + colon)
text_lines: list[str] = []
for line in raw.split("\n"):
if re.match(r"^\s*\w+:", line):
break
text_lines.append(line)
text = " ".join(
line.strip() for line in text_lines if line.strip()
)
# Extract output.url
url = ""
url_match = re.search(
r"^\s*output:\s*\n\s+url:\s*(.+?)\s*$", entry, re.MULTILINE
)
if url_match:
raw_path = url_match.group(1).strip().strip("'\"")
if raw_path and not raw_path.startswith("http"):
url = f"{base_url}/{raw_path.lstrip('/')}"
elif raw_path.startswith("http"):
url = raw_path
if url:
image: dict = {
"url": url,
"type": "image",
"nsfwLevel": 0,
"width": w,
"height": h,
"meta": {"prompt": text, "negativePrompt": ""},
"hasMeta": bool(text),
"hasPositivePrompt": bool(text),
}
images.append(image)
return images
def _extract_frontmatter(text: str) -> str:
"""Return the YAML frontmatter content (without the ``---`` delimiters).
Returns empty string when no frontmatter is found.
"""
if text.startswith("---"):
idx = text.find("---", 3)
if idx != -1:
return text[3:idx]
return ""
def convert_readme_to_html(markdown_text: str | None) -> str:
"""Convert HF README markdown to sanitised HTML."""
if not markdown_text: