anima-collection-mcp / jujo_browser.py
yamiuchi's picture
Upload validation app: cleaned prompt macros and public aggregate statistics
14fc583 verified
Raw History Blame Contribute Delete
8.74 kB
"""Responsive, WebUI-only author sample browser and explicit slot assignment."""
import math
import re
from pathlib import Path
from urllib.parse import quote
import gradio as gr
from jujo_loras import CATALOGS, JUJO_ENTRIES, NONE_LORA, PONY_LABEL, PONY_WARNING, lora_selected
ASSETS = Path(__file__).resolve().parent / "assets" / "jujo-browser"
def category(entry):
series = Path(entry["doc"]).parent.name
if series in {"doyagao", "inv_v_eyebrows"}:
return "眉"
if series in {"smallmouth", "widemouth", "mouthcollection", "talkmouth", "smallclosedmouth",
"smirkmouth", "smugmouth", "chestnutmouth", "circlemouth", "clenchedteeth"}:
return "口"
if series in {"darkness", "facelightup", "anti25d", "make25d"}:
return "効果"
if series in {"eyeshapes", "eyecollexl", "tareme", "tsurime", "jitome", "hosome", "closedeye",
"coldeyes", "pupil", "smirkingeye", "worriedeyes", "wink", "sleepy", "surprised",
"whiteeyelash", "eyelashext"} or entry["name"].startswith("hotarueye_"):
return "目の形"
return "表情・人物"
def entries_for_browser(family):
catalog = CATALOGS[family]
base = f"https://huggingface.co/{catalog['repo']}"
entries = []
for entry in JUJO_ENTRIES[family].values():
name = entry["name"]
pony_only = bool(entry.get("pony_only"))
code = name.removesuffix(".safetensors")
for suffix in ("_anima_v10", "_xl_v10", "_xl_v20", "_xl_v30"):
code = code.removesuffix(suffix)
hint = "強度や補助プロンプトは、作者の使い方をご確認ください。"
sample_note = "作者のシリーズ共通比較サンプル" if entry["group_sample"] else "作者の生成サンプル"
strength = 0.8 if family == "anima" else 1.0
pupil = re.fullmatch(r"hotarupupil_xl_type([1-6])_v10\.safetensors", name)
hosome = re.fullmatch(r"hotarueye_xl_hosome([12])_v30\.safetensors", name)
comic = re.fullmatch(r"hotarueye_xl_comic(0[1-4])_v\d+(?:_(?:ill|pony))?\.safetensors", name)
if family == "anima" and entry["doc"] == "eyeshapes/README.md":
hint = "作者の強度目安:0.3~1.5。複数併用時は合計1.0~1.5程度。closed eyesは避けてください。"
elif family == "illustrious" and entry["doc"] == "sdxl/eyecollexl/README.md":
hint = "作者のILL向け強度目安:1.5~2.0。種類に応じてtareme / tsurimeも参照してください。"
strength = 1.5
eye_code = re.fullmatch(r"eyecolle_xl_(code\d{3})", code)
if eye_code:
code = eye_code[1]
if entry["image"] and not entry["group_sample"]:
shape = "tareme" if Path(entry["image"]).name.startswith("tareme_") else "tsurime"
shape_label = "タレ目系" if shape == "tareme" else "吊り目系"
sample_note = f"{code}に対応する作者のIllustrious生成サンプル({shape_label})"
hint = f"作者のILL向け強度目安:1.5~2.0。掲載サンプルは{shape}をプロンプトに指定しています。"
elif family == "illustrious" and pupil:
code = f"Type {pupil[1]}"
sample_note = f"作者の比較画像から{code}の部分を表示(原図の比較画像も開けます)"
descriptions = {
"1": "大きな瞳・白目が少ないタイプ", "2": "寄り目・左右方向が細い瞳",
"3": "寄り目・正円に近い瞳", "4": "ハイライトが少なく黒目がちな印象",
"5": "左右方向が細く、上下端のカーブが見える瞳",
"6": "左右方向が細く、上下端のカーブが隠れる瞳",
}
hint = f"{code}:{descriptions[pupil[1]]}。作者の強度目安:0.5~2.0。追加プロンプトは基本的に不要です。"
elif family == "illustrious" and hosome:
code = f"Type {hosome[1]} / v3.0"
shape = "吊り目型・直線的な目尻" if hosome[1] == "1" else "タレ目型・曲線的な目尻"
sample_note = f"Type {hosome[1]}に対応する作者のv3.0サンプル(Illustrious向け)"
hint = f"{shape}。作者の強度目安:0.5~1.5。追加プロンプトは基本的に不要です。"
elif family == "illustrious" and comic:
code = f"Type {int(comic[1])} / {'Pony' if pony_only else 'ILL'}"
sample_note = f"Type {int(comic[1])}に対応する作者のタイプ別サンプル"
hint = "目の色や形などの詳細な記述はプロンプトに入れずに使用してください。"
if pony_only:
sample_note += "(Pony用LoRAの作者掲載画像。ILLでの再現は未確認)"
entries.append(dict(
name=name, title=entry["title"], code=code, category=category(entry),
image=f"{base}/resolve/{catalog['revision']}/{quote(entry['image'])}" if entry["image"] else None,
source=f"{base}/blob/{catalog['revision']}/{quote(entry['doc'])}",
group_sample=entry["group_sample"], sample_note=sample_note, hint=hint, strength=strength,
sample_viewport=entry.get("sample_viewport"),
pony_only=pony_only, compatibility_label=PONY_LABEL if pony_only else "",
compatibility_warning=PONY_WARNING if pony_only else "",
))
return entries
def slot_snapshot(values):
return [dict(enabled=lora_selected(values[i], values[i + 1]),
name=values[i + 1] or NONE_LORA, strength=float(values[i + 2]))
for i in range(0, 12, 3)]
def assignment(family, payload, values):
"""Validate the explicit Apply action using live Gradio slot values."""
if not isinstance(payload, dict) or payload.get("selected") not in JUJO_ENTRIES[family]:
raise gr.Error("このモードのサンプルからLoRAを選択してください。")
slot = payload.get("slot")
if isinstance(slot, bool) or not isinstance(slot, int) or not 1 <= slot <= 4:
raise gr.Error("適用先のLoRA 1~4を選択してください。")
try:
strength = float(payload["strength"])
except (KeyError, TypeError, ValueError):
raise gr.Error("強度を0~2の範囲で入力してください。") from None
if not math.isfinite(strength) or not 0 <= strength <= 2:
raise gr.Error("強度を0~2の範囲で入力してください。")
updated = list(values)
offset = (slot - 1) * 3
updated[offset:offset + 3] = [True, payload["selected"], strength]
return offset, updated
class JujoBrowser(gr.HTML):
def api_info(self):
return {"type": "object", "additionalProperties": True}
def build_jujo_browser(family, lora_components, lora_defaults):
initial = [value for slot in lora_defaults for value in slot]
with gr.Accordion("JujoHotaruのLoRAサンプル・使い方はこちら", open=False, elem_classes=["jujo-samples"]):
browser = JujoBrowser(
value={"slots": slot_snapshot(initial)}, entries=entries_for_browser(family),
html_template=(ASSETS / "browser.html").read_text(encoding="utf-8"),
css_template=(ASSETS / "browser.css").read_text(encoding="utf-8"),
js_on_load=(ASSETS / "browser.js").read_text(encoding="utf-8"),
apply_default_css=False, padding=False,
)
def sync_slots(*values):
return {"slots": slot_snapshot(values)}
def apply_sample(payload, *values):
offset, updated = assignment(family, payload, values)
outputs = [gr.skip() for _ in values]
outputs[offset:offset + 3] = [
gr.Checkbox(value=True, interactive=True),
gr.Dropdown(value=updated[offset + 1]),
gr.Slider(value=updated[offset + 2], interactive=True),
]
return (*outputs, {"slots": slot_snapshot(updated),
"message": f"LoRA {offset // 3 + 1}に設定して有効化しました。"})
gr.on(
triggers=[component.change for component in lora_components], fn=sync_slots,
inputs=lora_components, outputs=browser, queue=False,
trigger_mode="always_last", api_visibility="private",
)
browser.submit(
apply_sample, inputs=[browser, *lora_components],
outputs=[*lora_components, browser], queue=False,
api_visibility="private",
)
return browser