Flatten skill category directory structure
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
#!/usr/bin/env python3
|
||||
"""校验角色设计 design.md 格式是否符合规范。
|
||||
|
||||
用法:
|
||||
# 校验单个角色
|
||||
python validate_character_design.py .mv/{song}/{theme}/{character}/design.md
|
||||
|
||||
# 校验 theme 下所有角色
|
||||
python validate_character_design.py .mv/{song}/{theme}/
|
||||
|
||||
规则:
|
||||
- 必须有一级标题 # 角色名
|
||||
- 必须包含 6 个二级标题分区:外貌特征、性格气质、标志物件、生图设定、候选图、选定图
|
||||
- 外貌特征必须包含列表项
|
||||
- 生图设定必须包含:提示词、模型、参考图
|
||||
- 候选图必须有至少 1 条列表项
|
||||
- 候选图路径对应的文件必须存在
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REQUIRED_SECTIONS = ["外貌特征", "性格气质", "标志物件", "生图设定", "候选图", "选定图"]
|
||||
PROMPT_FIELDS = ["提示词:", "模型:", "参考图:"]
|
||||
|
||||
# 中英 section / field 映射,校验时兼容双语
|
||||
SECTION_ALIASES = {
|
||||
"外貌特征": "Appearance",
|
||||
"性格气质": "Personality",
|
||||
"标志物件": "Signature Items",
|
||||
"生图设定": "Image Settings",
|
||||
"候选图": "Candidates",
|
||||
"选定图": "Selected",
|
||||
}
|
||||
PROMPT_FIELD_ALIASES = {
|
||||
"提示词:": ("Prompt:", "Prompt:"),
|
||||
"模型:": ("Model:", "Model:"),
|
||||
"参考图:": ("Reference:", "Reference:"),
|
||||
}
|
||||
IMG_EXTS = {".png", ".jpg", ".jpeg", ".webp"}
|
||||
|
||||
H1_RE = re.compile(r"^#\s+(.+)$")
|
||||
H2_RE = re.compile(r"^##\s+(.+)$")
|
||||
LIST_RE = re.compile(r"^-\s+(.+)$")
|
||||
CANDIDATE_RE = re.compile(r"^-\s+(candidate_\S+)")
|
||||
|
||||
|
||||
def parse_design(text: str) -> dict:
|
||||
"""解析 design.md,返回结构化数据。"""
|
||||
lines = text.split("\n")
|
||||
result = {"title": None, "sections": {}}
|
||||
current_section = None
|
||||
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
|
||||
h1 = H1_RE.match(stripped)
|
||||
if h1:
|
||||
result["title"] = h1.group(1)
|
||||
continue
|
||||
|
||||
h2 = H2_RE.match(stripped)
|
||||
if h2:
|
||||
current_section = h2.group(1)
|
||||
result["sections"][current_section] = []
|
||||
continue
|
||||
|
||||
if current_section is not None:
|
||||
result["sections"][current_section].append(stripped)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_section(sections: dict, zh_name: str) -> list:
|
||||
"""从 sections 字典中获取内容,兼容中英 section header。"""
|
||||
en_name = SECTION_ALIASES.get(zh_name, "")
|
||||
return sections.get(zh_name, sections.get(en_name, []))
|
||||
|
||||
|
||||
def has_section(sections: dict, zh_name: str) -> bool:
|
||||
"""检查 section 是否存在,兼容中英。"""
|
||||
en_name = SECTION_ALIASES.get(zh_name, "")
|
||||
return zh_name in sections or en_name in sections
|
||||
|
||||
|
||||
def has_prompt_field(content: str, zh_field: str) -> bool:
|
||||
"""检查生图设定字段是否存在,兼容中英。"""
|
||||
if zh_field in content:
|
||||
return True
|
||||
for alias in PROMPT_FIELD_ALIASES.get(zh_field, ()):
|
||||
if alias in content:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def validate_one(design_path: Path) -> list[str]:
|
||||
"""校验单个 design.md,返回问题列表。"""
|
||||
issues = []
|
||||
char_dir = design_path.parent
|
||||
char_name = char_dir.name
|
||||
|
||||
text = design_path.read_text(encoding="utf-8")
|
||||
data = parse_design(text)
|
||||
|
||||
# 角色名
|
||||
if not data["title"]:
|
||||
issues.append(f"ERROR [{char_name}] 缺少一级标题 # 角色名")
|
||||
|
||||
# 必需分区
|
||||
for section in REQUIRED_SECTIONS:
|
||||
if not has_section(data["sections"], section):
|
||||
en = SECTION_ALIASES.get(section, "")
|
||||
issues.append(f"ERROR [{char_name}] 缺少分区 ## {section}/{en}")
|
||||
|
||||
# 外貌特征:需要列表项
|
||||
appearance = get_section(data["sections"], "外貌特征")
|
||||
if appearance:
|
||||
items = [l for l in appearance if LIST_RE.match(l)]
|
||||
if len(items) < 2:
|
||||
issues.append(f"WARN [{char_name}] 外貌特征/Appearance 列表项过少(至少 2 项)")
|
||||
|
||||
# 标志物件:需要列表项
|
||||
sig_items = get_section(data["sections"], "标志物件")
|
||||
if sig_items:
|
||||
items = [l for l in sig_items if LIST_RE.match(l)]
|
||||
if not items:
|
||||
issues.append(f"WARN [{char_name}] 标志物件/Signature Items 缺少列表项")
|
||||
|
||||
# 生图设定:需要提示词、模型、参考图
|
||||
img_settings = get_section(data["sections"], "生图设定")
|
||||
if img_settings:
|
||||
content = "\n".join(img_settings)
|
||||
for field in PROMPT_FIELDS:
|
||||
if not has_prompt_field(content, field):
|
||||
issues.append(f"ERROR [{char_name}] 生图设定/Image Settings 缺少字段 {field}")
|
||||
|
||||
# 候选图:需要至少 1 条,且文件存在
|
||||
candidates_lines = get_section(data["sections"], "候选图")
|
||||
if candidates_lines:
|
||||
candidates = []
|
||||
for line in candidates_lines:
|
||||
m = CANDIDATE_RE.match(line)
|
||||
if m:
|
||||
candidates.append(m.group(1))
|
||||
if not candidates:
|
||||
issues.append(f"ERROR [{char_name}] 候选图/Candidates 为空(需要至少 1 张)")
|
||||
for fname in candidates:
|
||||
actual_name = fname.split()[0] if " " in fname else fname
|
||||
fpath = char_dir / actual_name
|
||||
if not fpath.exists():
|
||||
issues.append(f"WARN [{char_name}] 候选图文件不存在: {actual_name}")
|
||||
|
||||
# 选定图:检查是否已选定
|
||||
selected_lines = get_section(data["sections"], "选定图")
|
||||
if selected_lines:
|
||||
content = "\n".join(selected_lines).strip()
|
||||
if not content:
|
||||
issues.append(f"INFO [{char_name}] 选定图/Selected 尚未填写(待用户挑选)")
|
||||
elif "selected" in content:
|
||||
sel_path = char_dir / "selected.jpg"
|
||||
sel_png = char_dir / "selected.png"
|
||||
if not sel_path.exists() and not sel_png.exists():
|
||||
issues.append(f"WARN [{char_name}] selected.jpg/png 文件不存在")
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
def find_designs(path: Path) -> list[Path]:
|
||||
"""查找所有 design.md 文件。"""
|
||||
if path.is_file() and path.name == "design.md":
|
||||
return [path]
|
||||
if path.is_dir():
|
||||
designs = []
|
||||
for d in sorted(path.iterdir()):
|
||||
if d.is_dir():
|
||||
dm = d / "design.md"
|
||||
if dm.exists():
|
||||
designs.append(dm)
|
||||
return designs
|
||||
return []
|
||||
|
||||
|
||||
def print_summary(all_issues: dict[str, list[str]]) -> None:
|
||||
print("=== 角色设计校验报告 ===\n")
|
||||
print(f"角色数量: {len(all_issues)}")
|
||||
print()
|
||||
|
||||
total_errors = 0
|
||||
total_warns = 0
|
||||
|
||||
for char_name, issues in all_issues.items():
|
||||
errors = [i for i in issues if "ERROR" in i]
|
||||
warns = [i for i in issues if "WARN" in i]
|
||||
infos = [i for i in issues if "INFO" in i]
|
||||
total_errors += len(errors)
|
||||
total_warns += len(warns)
|
||||
|
||||
status = "PASS" if not errors else "FAIL"
|
||||
print(f" [{status}] {char_name} — {len(errors)} 错误, {len(warns)} 警告")
|
||||
|
||||
print()
|
||||
if total_errors == 0 and total_warns == 0:
|
||||
no_info = all(not any("INFO" in i for i in iss) for iss in all_issues.values())
|
||||
if no_info:
|
||||
print("ALL PASS")
|
||||
else:
|
||||
print("ALL PASS (部分角色待用户选图)")
|
||||
else:
|
||||
all_issues_flat = [i for iss in all_issues.values() for i in iss]
|
||||
print(f"发现 {len(all_issues_flat)} 个问题:\n")
|
||||
for issue in all_issues_flat:
|
||||
print(f" {issue}")
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("用法:")
|
||||
print(" python validate_character_design.py <design.md 路径>")
|
||||
print(" python validate_character_design.py <theme 目录路径>")
|
||||
sys.exit(1)
|
||||
|
||||
target = Path(sys.argv[1]).resolve()
|
||||
if not target.exists():
|
||||
print(f"ERROR: 路径不存在: {target}")
|
||||
sys.exit(1)
|
||||
|
||||
designs = find_designs(target)
|
||||
if not designs:
|
||||
print(f"ERROR: 未找到 design.md 文件: {target}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"\n--- 角色设计校验 ---\n")
|
||||
|
||||
all_issues = {}
|
||||
for dp in designs:
|
||||
char_name = dp.parent.name
|
||||
issues = validate_one(dp)
|
||||
all_issues[char_name] = issues
|
||||
|
||||
print_summary(all_issues)
|
||||
|
||||
has_error = any("ERROR" in i for iss in all_issues.values() for i in iss)
|
||||
sys.exit(1 if has_error else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user