import gradio as gr
import re as _re
_GRADIO_MAJOR = int(_re.match(r"(\d+)", gr.__version__).group(1))
import html
from src.i18n import t
from src.prompt_parser import parse_prompt
from src.tag_warehouse import TagWarehouse, STYLE_ICONS
from src.variation_engine import generate_variations, generate_negative_prompt
from src.presets import PRESETS, PRESET_GROUPS, get_preset_categories, get_user_preset_names, save_user_preset, delete_user_preset
from src.prompt_analyzer import analyze_prompt, format_analysis_html
from src.session_history import HistoryEntry, add_to_history, get_history
from src.synonym_data import reload_synonym_groups
from src.tag_format import to_booru_tag, to_prompt_tag
from theme.whyx_theme import WhyxTheme
from src.ensemble_tagger import get_ensemble_tagger
from src.image_tagger import _TAGGER_DEPS_OK
from PIL import Image, ImageOps
MAX_OUTPUTS = 10
warehouse = TagWarehouse()
ALL_CATEGORIES = [
"quality", "lighting", "composition", "effects", "atmosphere",
"framing", "style", "colors", "background", "pose", "expression",
"clothing", "special_fx", "year_meta", "nsfw",
"animal", "furry", "object", "food",
"vehicle", "weapon", "architecture",
"demon", "angelic",
"hair", "body", "accessory", "season",
]
CATEGORY_ICONS = {
"quality": "โญ", "lighting": "๐ก", "composition": "๐",
"effects": "๐", "atmosphere": "๐", "framing": "๐ผ๏ธ",
"style": "๐จ", "colors": "๐", "background": "๐๏ธ",
"pose": "๐ง", "expression": "๐", "clothing": "๐",
"special_fx": "โจ", "year_meta": "๐
", "nsfw": "๐",
"animal": "๐พ", "furry": "๐บ", "object": "๐บ", "food": "๐",
"vehicle": "๐", "weapon": "โ๏ธ", "architecture": "๐๏ธ",
"demon": "๐ฟ", "angelic": "๐ผ",
"hair": "๐", "body": "๐ช", "accessory": "๐", "season": "๐ธ",
}
DEFAULT_CHECKED = {"quality", "lighting", "expression"}
def _build_artist_choice(a):
icon = STYLE_ICONS.get(a.get('style', ''), '')
style = a.get('style', '')
return f"{icon} {a['tag']} [{style}]" if style else f"{icon} {a['tag']}"
artist_styles = [""] + warehouse.get_artist_styles()
all_artists = warehouse.get_all_artists()
all_tandems = warehouse.get_all_tandems()
STYLECHOICES = [(s.title() if s else t("artist_all", "en"), s) for s in artist_styles]
ARTISTCHOICES = [
(_build_artist_choice(a), a["tag"])
for a in sorted(all_artists, key=lambda a: a.get("popularity", 0), reverse=True)[:50]
]
TANDEMCHOICES = [(t("tandem_placeholder", "en"), None)] + [
(
f"๐ค {tand['artists'][0]} ร {tand['artists'][1]} [{tand.get('style_blend','')}] ({tand.get('compatibility',0):.0%})",
i,
)
for i, tand in enumerate(all_tandems)
]
COPY_JS = """(text) => {
if (!text || !text.trim()) return text;
try {
navigator.clipboard.writeText(text);
} catch(e) {
try {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.focus();
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
} catch(e2) {}
}
const btn = document.activeElement;
if (btn && btn.classList.contains('whyx-copy-btn')) {
const origHTML = btn.innerHTML;
btn.innerHTML = '✓';
btn.style.borderColor = '#34D399';
btn.style.background = 'rgba(52,211,153,0.15)';
btn.style.transform = 'scale(1.1)';
setTimeout(() => {
btn.innerHTML = origHTML;
btn.style.borderColor = '';
btn.style.background = '';
btn.style.transform = '';
}, 1400);
}
return text;
}"""
def on_generate(prompt, model, rating, num_variations, creativity, weight_mode, mode, lang, artist_style, selected_artists, use_tandems, selected_tandem_idx, web_enrich, seed, current_preset_state, fx_count, blacklist_text, mirror_blacklist, strip_quality, strip_artist, strip_lora, strip_meta, min_tags, user_preset_state, output_format, *checks) -> tuple[str, ...]:
try:
if not prompt or not prompt.strip():
return tuple([""] * (MAX_OUTPUTS * 2))
parsed = parse_prompt(prompt)
if parsed is None:
msg = t("error_parse", lang)
return tuple([msg] + [""] * (MAX_OUTPUTS - 1) + [""] * MAX_OUTPUTS)
active = [c for c, on in zip(ALL_CATEGORIES, checks) if on]
if not active:
active = ["quality", "lighting", "expression"]
if rating in ("pg", "pg13") and "nsfw" in active:
active.remove("nsfw")
selected_tandem = None
if use_tandems and selected_tandem_idx is not None:
try:
idx = int(selected_tandem_idx)
if 0 <= idx < len(all_tandems):
selected_tandem = all_tandems[idx]
except (ValueError, TypeError):
pass
selected_presets = [p.strip() for p in (current_preset_state or "").split(",") if p.strip()]
if user_preset_state:
selected_presets.extend(
p.strip() for p in str(user_preset_state).split(",") if p.strip()
)
fx_count = {"off": 0, "light": 2, "rich": 4}.get(fx_count, 0)
# Blacklist / Tags-to-remove (A): comma- or newline-separated.
exclude_tags = [
t.strip().lower()
for t in str(blacklist_text or "").replace("\n", ",").split(",")
if t.strip()
]
mirror = bool(mirror_blacklist)
results = generate_variations(
parsed=parsed,
selected_categories=active,
num_variations=int(num_variations),
creativity=creativity,
model=model.lower(),
rating=rating,
warehouse=warehouse,
artist_style=artist_style or "",
selected_artists=selected_artists or [],
use_tandems=bool(use_tandems),
selected_tandem=selected_tandem,
weight_mode=weight_mode,
mode=mode,
web_enrich=web_enrich == "1",
selected_presets=selected_presets,
fx_count=fx_count,
seed=int(seed) if seed is not None and str(seed).strip() else None,
exclude_tags=exclude_tags,
strip_quality=bool(strip_quality),
strip_artist=bool(strip_artist),
strip_lora=bool(strip_lora),
strip_meta=bool(strip_meta),
min_tags=int(min_tags or 0),
output_format=output_format or "prompt",
)
out = list(results)
while len(out) < MAX_OUTPUTS:
out.append("")
neg_results = generate_negative_prompt(
parsed=parsed,
selected_categories=active,
num_variations=int(num_variations),
rating=rating,
warehouse=warehouse,
model=model.lower(),
positive_tags=out[:MAX_OUTPUTS],
extra_negative=exclude_tags if mirror else None,
output_format=output_format or "prompt",
)
neg_out = list(neg_results)
while len(neg_out) < MAX_OUTPUTS:
neg_out.append("")
add_to_history(
prompt, out[:MAX_OUTPUTS], model=model, rating=rating,
num_variations=int(num_variations), creativity=creativity,
weight_mode=weight_mode,
neg_results=neg_out[:MAX_OUTPUTS],
categories=active,
seed=int(seed) if seed is not None and str(seed).strip() else None,
)
return tuple(out[:MAX_OUTPUTS]) + tuple(neg_out[:MAX_OUTPUTS])
except Exception as exc:
import traceback
traceback.print_exc()
err_msg = t("generation_error", lang).format(exc=html.escape(str(exc)))
return tuple([err_msg] + [""] * (MAX_OUTPUTS - 1) + [""] * MAX_OUTPUTS)
def on_suggest(prompt, model, rating, lang) -> str:
"""Suggest improvement tags for the given prompt (read-only analysis)."""
try:
if not prompt or not prompt.strip():
return f'
{t("input_placeholder", "en" if lang != "RU" else "ru")}
'
parsed = parse_prompt(prompt)
if parsed is None:
return f'{t("error_parse", lang)}
'
from src.suggestion_engine import suggest_prompt_improvements
suggestions = suggest_prompt_improvements(
parsed, warehouse, model=model.lower(), rating=rating, max_suggestions=6
)
lc = "ru" if lang == "RU" else "en"
if not suggestions:
return f'{t("suggest_empty", lc)}
'
lines = "".join(
f'{html.escape(s)}' for s in suggestions
)
return (
f''
f'
{t("suggest_header", lc)}
'
f'
'
)
except Exception as exc:
import traceback
traceback.print_exc()
return f'Error: {html.escape(str(exc))}
'
def _preset_status(key: str, lang: str, **kwargs) -> str:
return f'{t(key, lang).format(**kwargs) if kwargs else t(key, lang)}
'
def on_user_preset_save(name: str, tags: str, lang: str):
name = (name or "").strip()
tags_list = [t.strip() for t in str(tags or "").replace("\n", ",").split(",") if t.strip()]
if not name:
return gr.update(choices=_user_preset_choices(lang), value=""), "", _preset_status("user_preset_need_name", lang)
save_user_preset(name, tags_list)
return gr.update(choices=_user_preset_choices(lang), value=name), "", _preset_status("user_preset_saved", lang, name=name)
def on_user_preset_apply(name: str, current_state: str, lang: str):
if not name:
return current_state, _preset_status("user_preset_pick", lang)
state = (current_state or "").strip()
keys = [k for k in state.split(",") if k.strip()] if state else []
if name not in keys:
keys.append(name)
return ",".join(keys), _preset_status("user_preset_applied", lang, name=name)
def on_user_preset_delete(name: str, lang: str):
if not name:
return gr.update(choices=_user_preset_choices(lang), value=""), _preset_status("user_preset_pick", lang)
delete_user_preset(name)
return gr.update(choices=_user_preset_choices(lang), value=""), _preset_status("user_preset_deleted", lang, name=name)
def on_web_search(query: str, lang: str) -> str:
from src.tag_searcher import search_tags
lc = "ru" if lang == "RU" else "en"
if not query or not query.strip():
return f'{t("web_search_empty", lc)}
'
try:
results = search_tags(query, warehouse)
parts = []
if results["local"]:
parts.append(f'๐ {t("web_search_offline", lc)} ({len(results["local"])})
')
for r in results["local"][:15]:
safe_tag = html.escape(r["tag"])
safe_cat = html.escape(r["category"])
parts.append(f'{safe_tag} [{safe_cat}]')
if results["web"]:
parts.append(f'๐ {t("web_search_online", lc)} ({len(results["web"])})
')
for r in results["web"][:MAX_OUTPUTS]:
safe_tag = html.escape(r.get("tag", ""))
count = r.get("post_count", 0)
parts.append(f'{safe_tag} ({count} posts)')
if not parts:
return f'{t("web_enrich_no_results", lc)}
'
return '' + "".join(parts) + '
'
except Exception as exc:
return f'{t("web_search_error", lc).format(exc=html.escape(str(exc)))}
'
def _format_history_html(lang: str) -> str:
lc = "ru" if lang == "RU" else "en"
history = get_history()
lines = []
lines.append("")
entries = history.get_all()[:5]
if not entries:
lines.append(f"
{t('history_empty', lc)}
")
else:
for entry in entries:
liked = entry.get_liked_results()
lines.append("
")
lines.append("
")
lines.append(f"{t('history_prompt', lc)}: ")
p = entry.prompt[:80] + "..." if len(entry.prompt) > 80 else entry.prompt
lines.append(f"{html.escape(p)}
")
if liked:
lines.append(f"
โฅ {len(liked)} {t('history_liked', lc).lower()}
")
lines.append("
")
favs = history.get_favorites()
if favs:
lines.append("
")
lines.append(f"
โฅ {t('favorites_label', lc)}
")
for entry, idx, text in favs[:5]:
t_text = text[:60] + "..." if len(text) > 60 else text
lines.append(f"
โฅ {html.escape(t_text)}
")
lines.append("
")
else:
lines.append("
")
lines.append(f"
{t('history_no_likes', lc)}
")
lines.append("
")
lines.append("
")
return "\n".join(lines)
def _heart_click(idx: int, lang, *results) -> tuple:
lc = "ru" if lang == "RU" else "en"
history = get_history()
entries = history.get_all()
heart_updates = []
for i in range(MAX_OUTPUTS):
if entries and i < len(entries[0].results):
heart_updates.append("โฅ" if entries[0].is_liked(i) else "โก")
else:
heart_updates.append("โก")
if entries and idx < len(results) and results[idx]:
liked = history.toggle_like(0, idx)
heart_updates[idx] = "โฅ" if liked else "โก"
return (gr.update(value=_format_history_html(lc)),) + tuple(gr.update(value=h) for h in heart_updates)
def on_preset_change(prompt, *group_and_state):
all_groups = list(group_and_state[:-1])
current_active = group_and_state[-1]
active = None
for vals in all_groups:
if vals and isinstance(vals, list) and len(vals) > 0:
active = vals[0]
break
if active == current_active:
resets = [gr.update(value=[]) for _ in PRESET_GROUPS]
return (*[c in DEFAULT_CHECKED for c in ALL_CATEGORIES], prompt, "", *resets)
if active:
cats = get_preset_categories(active)
resets = []
for group in PRESET_GROUPS:
if active in group["presets"]:
resets.append(gr.update(value=[active]))
else:
resets.append(gr.update(value=[]))
# Presets are now layered inside the engine (protected additions),
# so the prompt text itself is left untouched.
return (*[c in cats for c in ALL_CATEGORIES], prompt, active, *resets)
else:
resets = [gr.update(value=[]) for _ in PRESET_GROUPS]
return (*[c in DEFAULT_CHECKED for c in ALL_CATEGORIES], prompt, "", *resets)
def on_rating_change(rating: str, *checks) -> list[bool]:
checks = list(checks)
if rating in ("pg", "pg13"):
idx = ALL_CATEGORIES.index("nsfw")
if idx < len(checks):
checks[idx] = False
return (*checks,)
def on_artist_filter_change(style: str, query: str):
q = (query or "").lower().strip()
filtered = all_artists
if style:
filtered = [a for a in filtered if a.get("style") == style]
if q:
filtered = [
a for a in filtered
if q in a["tag"].lower()
or q in a.get("style", "").lower()
or any(q in t.lower() for t in a.get("signature_tags", []))
]
# Lazy-load: limit to top 50 by popularity unless searching
if not q and len(filtered) > 50:
filtered = sorted(filtered, key=lambda a: a.get("popularity", 0), reverse=True)[:50]
choices = [(_build_artist_choice(a), a["tag"]) for a in filtered]
return gr.update(choices=choices, value=[])
def _format_artist_info(artist_names: list[str], lang: str = "en") -> str:
title = t("artist_preview_title", lang)
if not artist_names:
empty = t("artist_preview_empty", lang)
return f"""
"""
lines = []
for name in artist_names:
safe_name = html.escape(name)
desc = warehouse.get_artist_description(name, lang)
url = warehouse.get_artist_danbooru_url(name)
if desc:
safe_url = html.escape(url) if url else ""
safe_desc = html.escape(desc)
link = f"{t('danbooru_link', lang)}" if url else ""
lines.append(f"""
{safe_name}
{link}
{safe_desc}
""")
# Similar artists from the first selected one
if len(artist_names) == 1:
similar = warehouse.get_similar_artists(artist_names[0], limit=3)
if similar:
names = ", ".join(html.escape(a["tag"]) for a in similar)
label = t("similar_artists", lang)
lines.append(f"โจ {label}: {names}
")
return "" + title + "
" + "".join(lines) + "
"
def on_artist_selection_change(selected_artists, lang):
return gr.update(value=_format_artist_info(selected_artists or [], lang))
def on_prompt_analyze(prompt: str, lang: str):
lc = "ru" if lang == "RU" else "en"
data = analyze_prompt(prompt, warehouse)
return gr.update(value=format_analysis_html(data, lc))
def _reload_data(lang):
warehouse.reload_pools()
warehouse.reload_artists()
reload_synonym_groups()
from src.prompt_rewriter import reload_map
from src.tag_searcher import reload_cooccurrence
from src.dedup_engine import reload_groups
reload_map()
reload_cooccurrence()
reload_groups()
def on_artist_style_recommendations(style: str, lang: str):
title = t("artist_preview_title", lang)
if not style:
empty = t("artist_preview_empty", lang)
return gr.update(value=f"""
""")
artists = warehouse.get_artists_by_style(style)
artists = sorted(artists, key=lambda a: a.get("popularity", 0), reverse=True)[:3]
if not artists:
empty = t("artist_preview_empty", lang)
return gr.update(value=f"""
""")
names = ", ".join(html.escape(a["tag"]) for a in artists)
label = t("top_artists_in_style", lang)
return gr.update(value=f"{title}
โจ {label}: {names}
")
def _build_tagger_html(result: dict, lc: str) -> str:
lines = []
lines.append("")
# ---- Natural-language caption (Florence) --------------------------------
nl_caption = (result.get("nl_caption") or "").strip()
if nl_caption:
lines.append(
f"
๐ฃ๏ธ {t('tagger_nl_caption', lc)}
"
f"
{html.escape(nl_caption)}
"
)
# ---- Pose summary ---------------------------------------------------------
pose_tags = result.get("pose_tags") or []
people = int(result.get("people_count") or 0)
if pose_tags or people:
pose_label = t("tagger_pose_single" if people == 1 else "tagger_pose_many", lc).format(n=people)
lines.append(f"
๐ง {t('tagger_pose_label', lc)}
")
if people:
lines.append(f"
{html.escape(pose_label)}
")
if pose_tags:
lines.append(
"
"
+ ", ".join(html.escape(p) for p in pose_tags)
+ "
"
)
# ---- Smart analysis tags (anatomy / expression / background / scene / depth)
for key, label_key, emoji, css_class in [
("anatomy_tags", "tagger_anatomy_label", "๐", "whyx-tagger-anatomy-tags"),
("expression_tags", "tagger_expression_label", "๐", "whyx-tagger-expr-tags"),
("background_tags", "tagger_background_label", "๐๏ธ", "whyx-tagger-bg-tags"),
("segmentation_tags", "tagger_segmentation_label", "๐", "whyx-tagger-seg-tags"),
("depth_tags", "tagger_depth_label", "๐", "whyx-tagger-depth-tags"),
]:
smart_tags = result.get(key) or []
if smart_tags:
lines.append(
f"
{emoji} {t(label_key, lc)}
"
)
lines.append(
f"
"
+ ", ".join(html.escape(p) for p in smart_tags)
+ "
"
)
# ---- Rating distribution ---------------------------------------------------
lines.append(f"
{t('tagger_ratings', lc)}
")
rating_colors = {"general": "#34D399", "sensitive": "#FBBF24", "questionable": "#F97316", "explicit": "#EF4444"}
for name, score in result["ratings"].items():
color = rating_colors.get(name, "var(--text-dim)")
pct = int(score * 100)
bar = "โฐ" * (pct // 10) + "โฑ" * (10 - pct // 10)
lines.append(f"
{name} {bar} ({score:.1%})
")
if result["characters"]:
lines.append(f"
{t('tagger_characters', lc)}
")
lines.append("
")
for name, score in list(result["characters"].items())[:6]:
safe = html.escape(name.replace("_", " ").replace("(", "\\(").replace(")", "\\)"))
lines.append(f"{safe} {score:.0%}")
lines.append("
")
lines.append(f"
{t('tagger_top_general', lc)}
")
lines.append("
")
for name, score in list(result["general"].items())[:14]:
safe = html.escape(name.replace("_", " ").replace("(", "\\(").replace(")", "\\)"))
pct = int(score * 100)
bar = "โฐ" * (pct // 10) + "โฑ" * (10 - pct // 10)
lines.append(f"
{bar}{safe}{score:.0%}
")
lines.append("
")
lines.append("
")
return "\n".join(lines)
def on_tag_image(image, gen_threshold, char_threshold, fmt, lang, progress=gr.Progress(), skip_pose=False, with_caption=False, caption_kind="florence", control_kind="skeleton_black", pose_mode="yolo", smart_analysis=True):
lc = "ru" if lang == "RU" else "en"
# fmt carries "prompt|" from the tagger-mode dropdown: "prompt|wd:eva02", "prompt|ensemble".
if "|" in fmt:
fmt, mode = fmt.split("|", 1)
fmt = fmt.strip() or "prompt"
mode = mode.strip() or "ensemble"
else:
mode = "ensemble"
empty_chips = gr.update(choices=[], value=[], visible=False)
hidden_control = (gr.update(visible=False), gr.update(visible=False))
if image is None:
return (
gr.update(value=f'{t("tagger_no_image", lc)}
'),
gr.update(visible=False),
empty_chips, empty_chips,
gr.update(visible=False),
gr.update(visible=False),
"", "",
) + hidden_control
try:
progress(0, desc=t("tagger_processing", lc))
if not _TAGGER_DEPS_OK:
msg = f'{t("tagger_unavailable", lc)}
'
return (
gr.update(value=msg),
gr.update(visible=False),
empty_chips, empty_chips,
gr.update(visible=False),
gr.update(visible=False),
"", "",
) + hidden_control
progress(0.35, desc=t("tagger_ratings_label", lc))
result = get_ensemble_tagger().tag_image(
image, gen_threshold, char_threshold,
mode=mode, skip_pose=skip_pose, with_caption=with_caption,
caption_kind=caption_kind, pose_mode=pose_mode,
smart_analysis=smart_analysis,
)
progress(0.9, desc=t("tagger_results", lc))
if not result["general"] and not result["characters"] and not result.get("nl_caption"):
html_out = f'{t("tagger_no_tags", lc)}
'
return (
gr.update(value=html_out),
gr.update(visible=False),
empty_chips, empty_chips,
gr.update(visible=False),
gr.update(visible=False),
"", "",
) + hidden_control
html_out = _build_tagger_html(result, lc)
general_names = list(result["general"].keys())[:60]
char_names = list(result["characters"].keys())
# Merge the NL caption into the tag chips so it can be picked up by
# the same apply/copy pipeline as booru tags. Pose tags are already
# folded into `general`/`caption`/`taglist` by the ensemble tagger,
# so they must NOT be added here again.
extra_names: list[str] = []
if result.get("nl_caption"):
extra_names.append(result["nl_caption"].strip())
all_names = extra_names + general_names
general_choices = [(n, n) for n in all_names]
char_choices = [(n.replace("_", " "), n) for n in char_names]
raw_caption = result["caption"]
esc_taglist = result["taglist"]
tags_box = esc_taglist if fmt == "prompt" else raw_caption
# ControlNet-style preview built from the same keypoints the pose tags
# came from (pure numpy / PIL, no extra models). Hidden when the pose
# pass was skipped or the preview kind has nothing to show.
control_outputs = _render_control_outputs(
ImageOps.exif_transpose(Image.fromarray(image)),
result, control_kind,
)
return (
gr.update(value=html_out),
gr.update(visible=True),
gr.update(choices=general_choices, value=all_names, visible=True),
gr.update(choices=char_choices, value=char_names, visible=True),
gr.update(value=tags_box, visible=True),
gr.update(visible=True),
raw_caption,
esc_taglist,
) + control_outputs
except Exception as exc:
import traceback
traceback.print_exc()
err = t("tagger_error", lc).format(exc=html.escape(str(exc)))
return (
gr.update(value=f'{err}
'),
gr.update(visible=False),
empty_chips, empty_chips,
gr.update(visible=False),
gr.update(visible=False),
"", "",
) + hidden_control
def _render_control_outputs(orig_pil, result, control_kind):
"""Build the (control image, download file) updates for the preview panel.
``orig_pil`` must be the EXIF-transposed input image so the overlay
matches the canvas the user uploaded. Returns two ``gr.update`` โ both
hidden when the preview has nothing to render (e.g. skeleton kind with no
pose run).
"""
try:
from src.control_preview import render_control, save_control_png
pil_out = render_control(
orig_pil, result.get("keypoints") or [], control_kind or "skeleton_black",
depth_map=result.get("depth_map"), seg_map=result.get("segmentation_map"),
)
if pil_out is None:
return gr.update(visible=False), gr.update(visible=False)
path = save_control_png(pil_out)
return gr.update(value=pil_out, visible=True), gr.update(value=path, visible=True)
except Exception:
return gr.update(visible=False), gr.update(visible=False)
def on_tag_nl_only(image, fmt, lang, caption_kind="florence", progress=gr.Progress()):
"""Tagger mode "nl"/"qwen": caption only, no WD14 / pose / rating passes.
The caption is the sole output โ it lands in the chips (single choice),
the tags box, the Apply/Copy pipeline and the raw/esc states, so the rest
of the Tagger UI keeps working unchanged. Character/control panels stay
hidden; empty or failed captions surface a warning instead.
"""
lc = "ru" if lang == "RU" else "en"
empty_chips = gr.update(choices=[], value=[], visible=False)
hidden_control = (gr.update(visible=False), gr.update(visible=False))
if image is None:
return (
gr.update(value=f'{t("tagger_no_image", lc)}
'),
gr.update(visible=False),
empty_chips, empty_chips,
gr.update(visible=False),
gr.update(visible=False),
"", "",
) + hidden_control
try:
progress(0, desc=t("tagger_captioning", lc))
if not _TAGGER_DEPS_OK:
msg = f'{t("tagger_unavailable", lc)}
'
return (
gr.update(value=msg),
gr.update(visible=False),
empty_chips, empty_chips,
gr.update(visible=False),
gr.update(visible=False),
"", "",
) + hidden_control
from src.vlm_caption import get_captioner
pil_img = ImageOps.exif_transpose(Image.fromarray(image))
caption = get_captioner().caption(pil_img, kind=caption_kind) or ""
progress(0.9, desc=t("tagger_results", lc))
if not caption:
html_out = f'{t("tagger_caption_empty", lc)}
'
return (
gr.update(value=html_out),
gr.update(visible=False),
empty_chips, empty_chips,
gr.update(visible=False),
gr.update(visible=False),
"", "",
) + hidden_control
nl_result = {
"general": {}, "characters": {}, "ratings": {},
"pose_tags": [], "people_count": 0, "nl_caption": caption,
}
return (
gr.update(value=_build_tagger_html(nl_result, lc)),
gr.update(visible=True),
gr.update(choices=[(caption, caption)], value=[caption], visible=True),
empty_chips,
gr.update(value=caption, visible=True),
gr.update(visible=True),
caption,
caption,
) + hidden_control
except Exception as exc:
import traceback
traceback.print_exc()
err = t("tagger_error", lc).format(exc=html.escape(str(exc)))
return (
gr.update(value=f'{err}
'),
gr.update(visible=False),
empty_chips, empty_chips,
gr.update(visible=False),
gr.update(visible=False),
"", "",
) + hidden_control
def _join_selected(chips, chars, fmt):
selected = (list(chips or []) + list(chars or []))
if fmt == "raw":
return ", ".join(to_booru_tag(t) for t in selected)
return ", ".join(to_prompt_tag(t) for t in selected)
def on_tagger_selection_change(chips, chars, fmt, raw_state, esc_state):
return _join_selected(chips, chars, fmt)
def on_tagger_format_change(fmt, chips, chars):
return _join_selected(chips, chars, fmt)
def on_tagger_apply_tags(current_prompt, tags):
if tags and isinstance(tags, str) and tags.strip():
if current_prompt and current_prompt.strip():
return f"{current_prompt.strip()}, {tags.strip()}"
return tags.strip()
return current_prompt if current_prompt else ""
HOTKEYS_JS = """
window.addEventListener('load', function() {
setTimeout(function() { window.dispatchEvent(new Event('resize')); }, 500);
setTimeout(function() { window.dispatchEvent(new Event('resize')); }, 1500);
});
document.addEventListener('keydown', function(e) {
const tag = document.activeElement ? document.activeElement.tagName.toLowerCase() : '';
const isTyping = tag === 'input' || tag === 'textarea' || document.activeElement.isContentEditable;
if (e.ctrlKey && e.shiftKey && e.key === 'Enter') {
e.preventDefault();
const btn = document.querySelector('.whyx-generate-btn button');
if (btn && !btn.disabled) btn.click();
return;
}
if (e.ctrlKey && e.shiftKey && (e.key === 'x' || e.key === 'X')) {
if (isTyping) {
e.preventDefault();
const ta = document.querySelector('textarea[data-testid="textbox"]');
if (ta) { ta.value = ''; ta.dispatchEvent(new Event('input', {bubbles:true})); }
}
return;
}
});
// Dropdown styling is done via CSS โ no JS, zero flicker
// Slide-down panel toggle (collapse/expand inline panels)
(function() {
function togglePanel(triggerBtn) {
let panelId = triggerBtn.getAttribute('data-panel-target');
if (!panelId && triggerBtn.id && triggerBtn.id.startsWith('whyx-trig-')) {
panelId = 'whyx-panel-' + triggerBtn.id.slice('whyx-trig-'.length);
}
if (!panelId) return;
const panel = document.getElementById(panelId);
if (!panel) return;
const isOpen = panel.classList.contains('whyx-slide-open');
if (isOpen) {
panel.classList.remove('whyx-slide-open');
panel.classList.add('whyx-slide-closed');
triggerBtn.classList.remove('whyx-slide-active');
} else {
panel.classList.add('whyx-slide-open');
panel.classList.remove('whyx-slide-closed');
triggerBtn.classList.add('whyx-slide-active');
}
}
document.addEventListener('click', function(e) {
let el = e.target;
while (el && el !== document.body) {
if (el.classList && el.classList.contains('whyx-slide-trigger')) {
e.preventDefault();
togglePanel(el);
return;
}
el = el.parentElement;
}
});
})();
"""
WHYX_CSS = """
:root {
--c0: #7C3AED; --c1: #4F46E5; --c2: #2563EB; --c3: #10B981;
--accent: linear-gradient(135deg, #7C3AED, #4F46E5 38%, #2563EB 70%, #10B981);
--accent-text: linear-gradient(135deg, #7C3AED, #4F46E5 38%, #2563EB 70%, #10B981);
--accent-soft: linear-gradient(135deg, rgba(124,58,237,0.18), rgba(79,70,229,0.16) 38%, rgba(37,99,235,0.16) 70%, rgba(16,185,129,0.18));
--accent-glow: rgba(79,70,229,0.18); --accent-border: rgba(124,58,237,0.35);
--bg: #070A12; --surface: rgba(14,19,32,0.88); --surface-hover: rgba(20,28,46,0.92);
--glass: rgba(14,19,32,0.65); --glass-border: rgba(255,255,255,0.07); --glass-border-hover: rgba(255,255,255,0.14);
--text: #E2E8F0; --text-dim: rgba(148,163,184,0.95); --text-faint: rgba(148,163,184,0.55); --text-accent: #7DEBD6;
--input-bg: rgba(10,14,26,0.55); --input-border: rgba(255,255,255,0.08); --input-border-focus: rgba(79,70,229,0.5); --input-text: rgba(226,232,240,0.92);
--chip-bg: rgba(255,255,255,0.045); --chip-border: rgba(255,255,255,0.09); --chip-text: #CBD5E1;
--radius-sm: 6px; --radius-md: 10px; --radius-lg: 16px; --radius-xl: 20px; --radius-pill: 999px;
--shadow-sm: 0 1px 4px rgba(0,0,0,0.22); --shadow-md: 0 4px 16px rgba(0,0,0,0.28); --shadow-lg: 0 8px 32px rgba(0,0,0,0.35); --shadow-glow: 0 0 20px rgba(79,70,229,0.12); --shadow-soft: 0 2px 8px rgba(0,0,0,0.15), 0 8px 24px rgba(79,70,229,0.06);
--transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1); --transition-fast: 0.12s ease;
}
body.whyx-light {
--bg: #EAEFF7; --surface: rgba(255,255,255,0.92); --surface-hover: rgba(255,255,255,0.96);
--glass: rgba(255,255,255,0.72); --glass-border: rgba(15,23,42,0.08); --glass-border-hover: rgba(15,23,42,0.15);
--text: #0F172A; --text-dim: rgba(71,85,105,0.95); --text-faint: rgba(100,116,139,0.65); --text-accent: #0EA5E9;
--input-bg: #FFFFFF; --input-border: rgba(15,23,42,0.14); --input-border-focus: rgba(14,165,233,0.5); --input-text: #0F172A;
--chip-bg: rgba(15,23,42,0.035); --chip-border: rgba(15,23,42,0.1); --chip-text: #334155;
--accent-glow: rgba(79,70,229,0.15); --accent-border: rgba(124,58,237,0.3);
}
body.whyx-light, body.whyx-light .gradio-container, body.whyx-light .dark {
--body-background-fill: #EAEFF7 !important; --body-background-fill-dark: #EAEFF7 !important;
--body-text-color: #0F172A !important; --body-text-color-dark: #0F172A !important;
--block-background-fill: #FFFFFF !important; --block-background-fill-dark: #FFFFFF !important;
--block-border-color: rgba(15,23,42,0.1) !important; --block-border-color-dark: rgba(15,23,42,0.1) !important;
--input-background-fill: #FFFFFF !important; --input-background-fill-dark: #FFFFFF !important;
--input-border-color: rgba(15,23,42,0.14) !important; --input-border-color-dark: rgba(15,23,42,0.14) !important;
--input-text-color: #0F172A !important; --input-text-color-dark: #0F172A !important;
--block-label-text-color: #475569 !important; --block-label-text-color-dark: #475569 !important;
--block-title-text-color: #1E293B !important; --block-title-text-color-dark: #1E293B !important;
--panel-background-fill: #FFFFFF !important; --panel-background-fill-dark: #FFFFFF !important;
--table-even-background-fill: #F1F5F9 !important; --table-even-background-fill-dark: #F1F5F9 !important;
--table-odd-background-fill: #FFFFFF !important; --table-odd-background-fill-dark: #FFFFFF !important;
--checkbox-background-color: #FFFFFF !important; --checkbox-background-color-dark: #FFFFFF !important;
--checkbox-border-color: rgba(15,23,42,0.18) !important; --checkbox-border-color-dark: rgba(15,23,42,0.18) !important;
--button-secondary-text-color: #1E293B !important; --button-secondary-text-color-dark: #1E293B !important;
--button-secondary-background-fill: transparent !important; --button-secondary-background-fill-hover: rgba(15,23,42,0.05) !important;
--button-secondary-border-color: rgba(15,23,42,0.16) !important;
}
@keyframes whyxBreath { 0%, 100% { opacity: 0.65; } 50% { opacity: 1; } }
@keyframes whyxFadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
@keyframes progressShimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
@media (prefers-reduced-motion: reduce) { body::before { animation: none !important; opacity: 1 !important; } * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
html, body, #root, .gradio-container { background: var(--bg) !important; font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif !important; }
body::before { content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none; background: radial-gradient(1200px 620px at 10% -10%, var(--accent-glow), transparent 58%), radial-gradient(1000px 520px at 100% 0%, rgba(37,99,235,0.12), transparent 55%), radial-gradient(1100px 700px at 50% 120%, rgba(16,185,129,0.10), transparent 60%); animation: whyxBreath 14s ease-in-out infinite; }
:focus-visible { outline: 2px solid rgba(90,200,245,0.6) !important; outline-offset: 2px !important; }
*::-webkit-scrollbar { width: 5px; height: 5px; } *::-webkit-scrollbar-track { background: rgba(15,23,42,0.25); } *::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 3px; } *::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.14); }
.whyx-section-card { background: var(--surface) !important; border: 1px solid var(--glass-border) !important; border-left: 3px solid rgba(110,231,210,0.18) !important; border-radius: var(--radius-lg) !important; box-shadow: var(--shadow-md), inset 0 1px 0 rgba(255,255,255,0.03) !important; transition: border-color var(--transition), box-shadow var(--transition), transform var(--transition) !important; overflow: visible !important; }
.whyx-section-card:hover { border-color: var(--glass-border-hover) !important; border-left-color: rgba(16,185,129,0.4) !important; box-shadow: var(--shadow-lg), var(--shadow-glow) !important; transform: translateY(-1px); }
.whyx-section-header { display: flex !important; align-items: flex-start !important; gap: 10px !important; margin: 0 0 14px !important; padding: 0 0 10px !important; border-bottom: 1px solid var(--glass-border) !important; animation: whyxFadeIn 0.3s ease both !important; }
.whyx-section-icon { font-size: 18px !important; line-height: 1.4 !important; flex-shrink: 0; }
.whyx-section-title { color: var(--text) !important; background: none !important; -webkit-background-clip: unset !important; -webkit-text-fill-color: unset !important; background-clip: unset !important; font-weight: 700 !important; font-size: 11px !important; letter-spacing: 1px !important; text-transform: uppercase !important; line-height: 1.4 !important; }
.whyx-section-desc { color: var(--text-dim) !important; font-size: 11px !important; line-height: 1.45 !important; margin-top: 2px !important; max-width: 640px !important; }
.whyx-title { background: linear-gradient(135deg, #7C3AED, #4F46E5 38%, #2563EB 70%, #10B981) !important; -webkit-background-clip: text !important; -webkit-text-fill-color: transparent !important; background-clip: text !important; color: transparent !important; display: inline-block !important; font-weight: 800 !important; font-size: 32px !important; letter-spacing: -0.6px !important; line-height: 1.2 !important; }
.whyx-subtitle { font-size: 12px !important; color: var(--text-dim) !important; margin-top: 2px !important; border-left: 2px solid rgba(110,231,210,0.45) !important; padding-left: 8px !important; }
.whyx-divider { height: 1px !important; background: linear-gradient(90deg, transparent, rgba(110,231,210,0.2), rgba(176,124,240,0.2), transparent) !important; margin: 10px 0 !important; border: none !important; }
.whyx-stepper { counter-reset: whyx-step; position: relative; gap: 4px; }
.whyx-stepper::before { content: ""; position: absolute; left: 25px; top: 30px; bottom: 44px; width: 2px; background: linear-gradient(180deg, var(--c0), var(--c1) 50%, var(--c3)); border-radius: 2px; opacity: 0.35; }
.whyx-step { position: relative; counter-increment: whyx-step; background: var(--surface) !important; border: 1px solid var(--glass-border) !important; border-radius: var(--radius-xl) !important; padding: 16px 18px 16px 60px !important; margin-bottom: 10px !important; box-shadow: var(--shadow-sm) !important; transition: border-color var(--transition), box-shadow var(--transition), transform var(--transition) !important; }
.whyx-step:hover { border-color: var(--glass-border-hover) !important; box-shadow: var(--shadow-md), 0 0 16px rgba(79,70,229,0.08) !important; transform: translateY(-1px); }
.whyx-step::before { content: counter(whyx-step); position: absolute; left: 10px; top: 50%; transform: translateY(-50%); width: 32px; height: 32px; border-radius: 50%; background: var(--accent); color: #06121A; font-weight: 800; font-size: 14px; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 14px rgba(79,70,229,0.25); z-index: 2; }
.whyx-step:hover::before { box-shadow: 0 6px 20px rgba(79,70,229,0.4); }
.whyx-step .whyx-field, .whyx-step .whyx-field:hover { border: none !important; box-shadow: none !important; transform: none !important; }
.whyx-step .whyx-field:focus-within { box-shadow: 0 0 0 2px rgba(79,70,229,0.14) !important; }
.whyx-field { border: 1px solid var(--input-border) !important; border-radius: var(--radius-md) !important; box-shadow: var(--shadow-sm) !important; transition: border-color var(--transition), box-shadow var(--transition), transform var(--transition) !important; }
.whyx-field:hover { border-color: var(--accent-border) !important; box-shadow: 0 0 0 3px var(--accent-glow), var(--shadow-sm) !important; transform: translateY(-1px); }
.whyx-field textarea, .whyx-field input[type="text"] { border: 1px solid var(--input-border) !important; border-radius: var(--radius-md) !important; box-shadow: var(--shadow-sm) !important; transition: border-color var(--transition), box-shadow var(--transition) !important; }
.whyx-field textarea:hover, .whyx-field input[type="text"]:hover { border-color: var(--accent-border) !important; }
.whyx-field:focus-within { border-color: var(--input-border-focus) !important; box-shadow: 0 0 0 3px var(--accent-glow), var(--shadow-md) !important; }
#root input[type="text"], #root textarea, .gradio-container .wrap input, .gradio-dropdown input, .gradio-textbox textarea, .gradio-container textarea { border-radius: var(--radius-md) !important; transition: border-color var(--transition), box-shadow var(--transition), background var(--transition) !important; }
#root textarea:focus, #root input[type="text"]:focus, .gradio-dropdown input:focus { border-color: var(--input-border-focus) !important; box-shadow: 0 0 0 3px var(--accent-glow), var(--shadow-md) !important; }
.gradio-dropdown, .gradio-slider, .gradio-radio, .gradio-checkboxgroup, .gradio-accordion { border-radius: var(--radius-md) !important; }
input[type="radio"], input[type="checkbox"] { accent-color: #5AC8F5 !important; }
.whyx-radio-group label { background: var(--chip-bg) !important; border: 1px solid var(--chip-border) !important; border-radius: var(--radius-sm) !important; padding: 5px 12px !important; transition: all var(--transition-fast) !important; cursor: pointer !important; box-shadow: none !important; }
.whyx-radio-group label:hover { background: rgba(255,255,255,0.06) !important; border-color: var(--glass-border-hover) !important; }
.whyx-radio-group label.selected { background: var(--accent-glow) !important; border-color: var(--accent-border) !important; color: var(--text-accent) !important; }
input[type="range"]::-webkit-slider-runnable-track { height: 4px !important; background: var(--glass-border) !important; border-radius: 2px !important; border: none !important; }
input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none !important; appearance: none !important; width: 16px !important; height: 16px !important; border-radius: 50% !important; background: linear-gradient(135deg, var(--c0), var(--c2)) !important; border: 2px solid var(--bg) !important; cursor: pointer !important; box-shadow: 0 1px 4px rgba(0,0,0,0.3) !important; margin-top: -6px !important; transition: all var(--transition-fast) !important; }
input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.15) !important; box-shadow: 0 2px 8px rgba(79,70,229,0.3) !important; }
input[type="range"]::-moz-range-track { height: 4px !important; background: rgba(255,255,255,0.1) !important; border-radius: 2px !important; border: none !important; }
input[type="range"]::-moz-range-thumb { width: 16px !important; height: 16px !important; border-radius: 50% !important; background: linear-gradient(135deg, var(--c0), var(--c2)) !important; border: 2px solid var(--bg) !important; cursor: pointer !important; box-shadow: 0 1px 4px rgba(0,0,0,0.3) !important; }
#root button { border-radius: var(--radius-pill) !important; box-shadow: var(--shadow-sm) !important; transition: transform var(--transition), box-shadow var(--transition), background var(--transition), filter var(--transition) !important; }
#root button:hover:not(:disabled) { transform: translateY(-2px) !important; box-shadow: 0 6px 20px rgba(79,70,229,0.18) !important; }
#root button:active:not(:disabled) { transform: translateY(0) scale(0.97) !important; box-shadow: var(--shadow-sm) !important; }
.whyx-generate-btn button { background: linear-gradient(135deg, var(--c0), var(--c1) 50%, var(--c2)) !important; background-image: none !important; border: none !important; color: #06121A !important; font-weight: 700 !important; letter-spacing: 0.3px !important; box-shadow: 0 4px 16px rgba(79,70,229,0.25) !important; text-shadow: 0 1px 2px rgba(255,255,255,0.15); }
.whyx-generate-btn button:hover:not(:disabled) { background: linear-gradient(135deg, #9D7CFA, #5B6BFA 50%, #4F8BFA) !important; box-shadow: 0 8px 28px rgba(79,70,229,0.32) !important; }
.whyx-copy-btn { min-width: 40px !important; width: 40px !important; height: 34px !important; border-radius: var(--radius-sm) !important; border: 1px solid var(--chip-border) !important; background: var(--chip-bg) !important; transition: all var(--transition-fast) !important; display: flex !important; align-items: center !important; justify-content: center !important; }
.whyx-copy-btn:hover { border-color: var(--accent-border) !important; background: var(--accent-glow) !important; }
.whyx-heart-btn { min-width: 34px !important; width: 34px !important; height: 34px !important; border-radius: var(--radius-sm) !important; border: 1px solid rgba(244,63,94,0.12) !important; background: var(--chip-bg) !important; transition: all var(--transition-fast) !important; display: flex !important; align-items: center !important; justify-content: center !important; font-size: 14px !important; color: #F43F5E !important; }
.whyx-heart-btn:hover { background: rgba(244,63,94,0.08) !important; border-color: rgba(244,63,94,0.25) !important; }
.whyx-cat-btn { flex: 1 !important; min-width: 0 !important; margin: 0 !important; }
.whyx-cat-btn label { display: flex !important; align-items: center !important; justify-content: center !important; gap: 4px !important; background: var(--chip-bg) !important; border: 1px solid var(--chip-border) !important; border-radius: var(--radius-pill) !important; padding: 8px 12px !important; cursor: pointer !important; transition: all var(--transition-fast) !important; font-size: 11px !important; font-weight: 500 !important; color: var(--text-dim) !important; text-align: center !important; user-select: none !important; min-height: 34px !important; height: auto !important; width: 100% !important; box-shadow: var(--shadow-sm) !important; }
.whyx-cat-btn input[type="checkbox"] { display: none !important; width: 0 !important; height: 0 !important; margin: 0 !important; padding: 0 !important; opacity: 0 !important; position: absolute !important; pointer-events: none !important; }
.whyx-cat-btn label > span:first-child { display: none !important; }
.whyx-cat-btn label:hover { background: rgba(255,255,255,0.06) !important; border-color: var(--glass-border-hover) !important; }
.whyx-cat-btn:has(input:checked) label { background: var(--accent-glow) !important; border-color: var(--accent-border) !important; color: var(--text-accent) !important; box-shadow: 0 0 14px rgba(79,70,229,0.25) !important; }
.whyx-cat-btn:has(input:checked) label:hover { border-color: rgba(110,231,210,0.55) !important; }
.whyx-preset-group-label { font-size: 12px !important; font-weight: 600 !important; color: var(--text) !important; margin-top: 10px !important; margin-bottom: 6px !important; }
.whyx-preset-toggle .gr-block { display: flex !important; flex-wrap: wrap !important; gap: 6px !important; }
.whyx-preset-toggle label { display: inline-flex !important; align-items: center !important; border: 1px solid var(--chip-border) !important; background: var(--chip-bg) !important; border-radius: var(--radius-pill) !important; padding: 5px 14px !important; font-size: 11px !important; cursor: pointer !important; transition: all var(--transition-fast) !important; user-select: none !important; white-space: nowrap !important; }
.whyx-preset-toggle label:hover { background: rgba(255,255,255,0.06) !important; border-color: var(--glass-border-hover) !important; }
.whyx-preset-toggle input[type="checkbox"]:checked + span { color: var(--text-accent) !important; }
.whyx-preset-toggle label:has(input:checked) { border-color: var(--accent-border) !important; background: var(--accent-glow) !important; box-shadow: 0 0 10px rgba(79,70,229,0.18) !important; }
.whyx-result-card { background: var(--surface) !important; border: 1px solid var(--glass-border) !important; border-radius: var(--radius-lg) !important; padding: 10px 14px !important; margin: 5px 0 !important; transition: all var(--transition) !important; box-shadow: var(--shadow-sm) !important; animation: whyxFadeIn 0.3s ease both !important; }
.whyx-result-card:hover { border-color: var(--glass-border-hover) !important; box-shadow: var(--shadow-md), var(--shadow-glow) !important; }
.whyx-result-card.empty { opacity: 0.5 !important; border-style: dashed !important; }
.whyx-result-num { background: var(--chip-bg) !important; color: var(--text-accent) !important; font-weight: 600 !important; font-size: 11px !important; padding: 4px 8px !important; border-radius: var(--radius-sm) !important; text-align: center !important; min-width: 28px !important; border: 1px solid var(--chip-border) !important; }
.whyx-result-text { background: transparent !important; border: none !important; font-family: 'JetBrains Mono', ui-monospace, monospace !important; font-size: 12px !important; line-height: 1.55 !important; color: var(--text) !important; padding: 4px 0 !important; }
.whyx-result-text::placeholder { color: var(--text-faint) !important; font-style: italic !important; font-family: 'Inter', ui-sans-serif, system-ui, sans-serif !important; }
.whyx-neg-text { background: transparent !important; border: none !important; font-family: 'JetBrains Mono', ui-monospace, monospace !important; font-size: 11px !important; line-height: 1.45 !important; color: var(--text-dim) !important; padding: 4px 0 !important; }
.tabitem { border-color: var(--glass-border) !important; border-radius: 0 var(--radius-lg) var(--radius-lg) var(--radius-lg) !important; }
.tabs { border-bottom: 1px solid var(--glass-border) !important; }
.tabs button { border-radius: 0 !important; border: none !important; border-bottom: 2px solid transparent !important; background: transparent !important; transition: all var(--transition-fast) !important; font-weight: 500 !important; font-size: 12px !important; }
.tabs button:hover { background: var(--chip-bg) !important; }
.tabs button.selected { background: transparent !important; color: var(--text-accent) !important; border-bottom: 2px solid transparent !important; box-shadow: inset 0 -2px 0 rgba(110,231,210,0.65) !important; }
.whyx-lang-btn { gap: 0 !important; justify-content: flex-end !important; margin-top: 0 !important; min-width: 100px !important; }
.whyx-lang-btn label { border: 1px solid var(--chip-border) !important; background: var(--chip-bg) !important; border-radius: var(--radius-sm) !important; padding: 3px 10px !important; font-size: 11px !important; font-weight: 600 !important; line-height: 1 !important; min-height: 0 !important; margin: 0 !important; transition: all var(--transition-fast) !important; cursor: pointer !important; }
.whyx-lang-btn label:hover { background: var(--accent-glow) !important; border-color: rgba(124,58,237,0.25) !important; }
.whyx-lang-btn label.selected { background: var(--accent-glow) !important; border-color: var(--accent-border) !important; color: var(--text-accent) !important; }
.whyx-theme-btn { gap: 0 !important; justify-content: center !important; margin-top: 0 !important; min-width: 110px !important; }
.whyx-theme-btn label { border: 1px solid var(--chip-border) !important; background: var(--chip-bg) !important; border-radius: var(--radius-sm) !important; padding: 3px 8px !important; font-size: 11px !important; font-weight: 600 !important; line-height: 1 !important; min-height: 0 !important; margin: 0 !important; transition: all var(--transition-fast) !important; cursor: pointer !important; }
.whyx-theme-btn label:hover { background: var(--accent-glow) !important; border-color: rgba(124,58,237,0.25) !important; }
.whyx-theme-btn label.selected { background: var(--accent-glow) !important; border-color: var(--accent-border) !important; color: var(--text-accent) !important; }
.whyx-info-text, .gradio-container .info { color: var(--text-dim) !important; font-size: 11px !important; line-height: 1.45 !important; border-left: 2px solid rgba(110,231,210,0.35) !important; background: var(--chip-bg) !important; padding: 6px 10px !important; border-radius: 0 var(--radius-sm) var(--radius-sm) 0 !important; margin-top: 4px !important; }
.whyx-info-highlight { color: var(--text-accent) !important; font-weight: 600 !important; }
.whyx-tooltip { position: relative !important; cursor: pointer !important; }
.whyx-tooltip:hover .whyx-tooltip-text, .whyx-tooltip:focus-within .whyx-tooltip-text { visibility: visible !important; opacity: 1 !important; }
.whyx-tooltip-text { visibility: hidden !important; opacity: 0 !important; position: absolute !important; z-index: 9999 !important; padding: 4px 8px !important; background: var(--surface) !important; color: var(--text) !important; font-size: 10px !important; font-weight: 600 !important; border-radius: var(--radius-sm) !important; border: 1px solid var(--glass-border) !important; white-space: nowrap !important; pointer-events: none !important; transition: opacity 0.15s ease !important; box-shadow: var(--shadow-md) !important; border-left: 2px solid rgba(110,231,210,0.35) !important; bottom: 110% !important; left: 50% !important; transform: translateX(-50%) !important; }
.whyx-tooltip-text::after { content: "" !important; position: absolute !important; top: 100% !important; left: 50% !important; margin-left: -4px !important; border-width: 4px !important; border-style: solid !important; border-color: var(--surface) transparent transparent transparent !important; }
.whyx-tagger-image .upload, .whyx-tagger-image { border-radius: var(--radius-lg) !important; overflow: hidden !important; border: 1px solid rgba(90,200,245,0.14) !important; box-shadow: var(--shadow-md) !important; }
.whyx-tagger-run-btn button { background: var(--accent) !important; background-image: none !important; color: #06121A !important; font-weight: 700 !important; letter-spacing: 0.3px !important; border: none !important; box-shadow: 0 4px 16px rgba(79,70,229,0.22) !important; }
.whyx-tagger-run-btn button:hover:not(:disabled) { background: linear-gradient(135deg, #9D7CFA, #5B6BFA, #3B8BE0) !important; box-shadow: 0 8px 24px rgba(37,99,235,0.3) !important; }
.whyx-tagger-apply-btn button { background: var(--accent) !important; border: none !important; color: #06121A !important; font-weight: 700 !important; font-size: 13px !important; padding: 8px 20px !important; border-radius: var(--radius-pill) !important; transition: all var(--transition) !important; }
.whyx-tagger-apply-btn button:hover { background: linear-gradient(135deg, #9D7CFA, #5B6BFA, #3B8BE0) !important; box-shadow: 0 6px 20px rgba(79,70,229,0.35) !important; transform: translateY(-1px) !important; }
.whyx-tagger-copy-btn button { background: var(--chip-bg) !important; border: 1px solid var(--chip-border) !important; color: var(--chip-text) !important; font-weight: 600 !important; box-shadow: none !important; }
.whyx-tagger-copy-btn button:hover:not(:disabled) { background: var(--accent-glow) !important; border-color: var(--accent-border) !important; color: var(--text-accent) !important; box-shadow: 0 4px 14px rgba(79,70,229,0.15) !important; }
.whyx-tagger-tags-card { border-radius: var(--radius-lg) !important; border: 1px solid rgba(129,140,248,0.14) !important; box-shadow: var(--shadow-md) !important; background: var(--surface) !important; }
.whyx-tagger-tags-box textarea { font-family: 'JetBrains Mono', ui-monospace, monospace !important; font-size: 12px !important; line-height: 1.5 !important; color: var(--input-text) !important; background: var(--input-bg) !important; border-radius: var(--radius-md) !important; }
.whyx-tagger-chips .wrap, .whyx-tagger-chips .grid { display: flex !important; flex-wrap: wrap !important; gap: 6px !important; max-height: 260px !important; overflow-y: auto !important; padding: 4px 2px 8px !important; }
.whyx-tagger-chips label { display: inline-flex !important; align-items: center !important; gap: 6px !important; border: 1px solid var(--chip-border) !important; background: var(--chip-bg) !important; border-radius: var(--radius-pill) !important; padding: 5px 12px !important; font-size: 12px !important; font-weight: 500 !important; color: var(--chip-text) !important; cursor: pointer !important; transition: all var(--transition-fast) !important; user-select: none !important; }
.whyx-tagger-chips label:hover { background: var(--accent-glow) !important; border-color: var(--accent-border) !important; transform: translateY(-1px) !important; }
.whyx-tagger-chips input[type="checkbox"] { width: 14px !important; height: 14px !important; accent-color: #5AC8F5 !important; margin: 0 2px 0 0 !important; }
.whyx-tagger-chips label:has(input:checked) { background: var(--accent-soft) !important; border-color: rgba(110,231,210,0.55) !important; color: #E6FBF5 !important; box-shadow: 0 0 10px rgba(79,70,229,0.18) !important; }
.whyx-tagger-panel { background: var(--input-bg) !important; border: 1px solid rgba(79,70,229,0.1) !important; border-radius: var(--radius-md) !important; padding: 12px 16px !important; margin-top: 4px !important; animation: whyxFadeIn 0.3s ease both !important; }
.whyx-tagger-panel-title { font-size: 12px !important; font-weight: 600 !important; color: var(--text) !important; margin-bottom: 6px !important; }
.whyx-tagger-rating-row { font-size: 11px !important; color: var(--text-dim) !important; margin-bottom: 2px !important; }
.whyx-tagger-rating-name { width: 92px !important; display: inline-block !important; font-weight: 600 !important; }
.whyx-tagger-rating-bar { font-family: monospace !important; letter-spacing: -1px !important; }
.whyx-tagger-rating-pct { color: var(--text-faint) !important; }
.whyx-tagger-badges { display: flex !important; flex-wrap: wrap !important; gap: 6px !important; }
.whyx-tagger-char-badge { display: inline-block !important; background: linear-gradient(135deg, rgba(129,140,248,0.18), rgba(99,102,241,0.14)) !important; border: 1px solid rgba(129,140,248,0.3) !important; border-radius: var(--radius-pill) !important; padding: 3px 10px !important; font-size: 12px !important; color: #E0E7FF !important; }
.whyx-tagger-badge-pct { color: #A5B4FC !important; font-size: 10px !important; margin-left: 2px !important; }
.whyx-tagger-gen-list { display: flex !important; flex-direction: column !important; gap: 1px !important; }
.whyx-tagger-gen-row { display: flex !important; align-items: center !important; gap: 8px !important; font-size: 11px !important; color: var(--text-dim) !important; white-space: nowrap !important; }
.whyx-tagger-gen-bar { color: #5AC8F5 !important; font-family: monospace !important; width: 90px !important; text-align: right !important; flex-shrink: 0 !important; overflow: hidden !important; }
.whyx-tagger-gen-name { overflow: hidden !important; text-overflow: ellipsis !important; }
.whyx-tagger-pose-tags { font-size: 12px !important; color: var(--text-dim) !important; line-height: 1.4 !important; background: var(--input-bg) !important; border: 1px solid rgba(79,70,229,0.1) !important; border-radius: var(--radius-sm) !important; padding: 6px 10px !important; margin-top: 4px !important; }
.whyx-tagger-pose-people { font-size: 11px !important; color: var(--text) !important; font-weight: 600 !important; }
.whyx-tagger-nl-caption { font-size: 12px !important; color: var(--text) !important; line-height: 1.4 !important; background: var(--input-bg) !important; border: 1px solid rgba(79,70,229,0.1) !important; border-radius: var(--radius-sm) !important; padding: 6px 10px !important; }
.whyx-tagger-anatomy-tags { font-size: 12px !important; color: var(--text-dim) !important; line-height: 1.4 !important; background: var(--input-bg) !important; border: 1px solid rgba(79,70,229,0.1) !important; border-radius: var(--radius-sm) !important; padding: 6px 10px !important; margin-top: 4px !important; }
.whyx-tagger-expr-tags { font-size: 12px !important; color: var(--text-dim) !important; line-height: 1.4 !important; background: var(--input-bg) !important; border: 1px solid rgba(79,70,229,0.1) !important; border-radius: var(--radius-sm) !important; padding: 6px 10px !important; margin-top: 4px !important; }
.whyx-tagger-bg-tags { font-size: 12px !important; color: var(--text-dim) !important; line-height: 1.4 !important; background: var(--input-bg) !important; border: 1px solid rgba(79,70,229,0.1) !important; border-radius: var(--radius-sm) !important; padding: 6px 10px !important; margin-top: 4px !important; }
.progress-level .progress-bar { background: var(--accent) !important; background-size: 200% auto !important; animation: progressShimmer 2s linear infinite !important; }
.progress-level .progress-text { color: var(--text) !important; font-size: 12px !important; }
.progress-level { background: rgba(15,23,42,0.5) !important; border: 1px solid rgba(124,58,237,0.12) !important; border-radius: var(--radius-md) !important; padding: 8px 12px !important; }
.whyx-accordion-group { gap: 8px !important; }
.whyx-accordion-group > .gradio-accordion { flex: 1 !important; min-width: 0 !important; }
.whyx-accordion-group .gradio-accordion { background: var(--surface) !important; border: 1px solid var(--glass-border) !important; border-radius: var(--radius-md) !important; }
.whyx-accordion-group .gradio-accordion label[class*="accordion"],
.whyx-accordion-group .gradio-accordion button,
.whyx-accordion-group .gradio-accordion summary {
color: var(--text) !important; font-weight: 600 !important; font-size: 12px !important; padding: 8px 12px !important; border-bottom: 1px solid var(--glass-border) !important; cursor: pointer !important; background: transparent !important;
}
.whyx-accordion-group .gradio-accordion > div:not(label) { padding: 8px 12px !important; }
.whyx-section-card, .gradio-dropdown, .gradio-dropdown > div, .tabs, .tabitem, [data-testid="tab-item"] { overflow: visible !important; }
/* --- Dropdown menu styling โ opaque panel, themed --- */
[role="listbox"], ul.options, ul.dropdown-options, [data-testid="dropdown-options"] {
z-index: 999999 !important;
background: rgba(10,14,26,0.98) !important;
border: 1px solid rgba(79,70,229,0.15) !important;
border-radius: 10px !important;
box-shadow: 0 8px 32px rgba(0,0,0,0.6), 0 0 0 1px rgba(79,70,229,0.06) !important;
padding: 4px 0 !important;
max-height: 320px !important;
overflow-y: auto !important;
animation: whyxDropdownIn 0.12s ease-out !important;
transform-origin: top center !important;
}
@keyframes whyxDropdownIn {
from { opacity: 0; transform: scaleY(0.92) translateY(-4px); }
to { opacity: 1; transform: scaleY(1) translateY(0); }
}
/* Custom scrollbar for dropdown */
[role="listbox"]::-webkit-scrollbar,
ul.options::-webkit-scrollbar { width: 4px !important; }
[role="listbox"]::-webkit-scrollbar-track,
ul.options::-webkit-scrollbar-track { background: transparent !important; }
[role="listbox"]::-webkit-scrollbar-thumb,
ul.options::-webkit-scrollbar-thumb { background: rgba(79,70,229,0.25) !important; border-radius: 2px !important; }
[role="listbox"]::-webkit-scrollbar-thumb:hover,
ul.options::-webkit-scrollbar-thumb:hover { background: rgba(79,70,229,0.4) !important; }
/* Dropdown items */
[role="option"], .gradio-dropdown li, .gradio-dropdown .item {
padding: 8px 14px !important;
font-size: 13px !important;
color: var(--text) !important;
cursor: pointer !important;
transition: background 0.1s ease, color 0.1s ease !important;
border-left: 2px solid transparent !important;
}
[role="option"]:hover, [role="option"].active,
.gradio-dropdown li:hover, .gradio-dropdown .item:hover, .gradio-dropdown .active {
background: linear-gradient(90deg, rgba(79,70,229,0.1), transparent) !important;
color: var(--text-accent) !important;
border-left-color: rgba(79,70,229,0.4) !important;
}
/* Dropdown trigger (the visible field) */
.gradio-dropdown {
background: rgba(10,14,26,0.85) !important;
border: 1px solid var(--input-border) !important;
box-shadow: var(--shadow-sm) !important;
transition: border-color var(--transition), box-shadow var(--transition) !important;
}
.gradio-dropdown:hover {
border-color: rgba(79,70,229,0.25) !important;
}
.gradio-dropdown:focus-within,
.gradio-dropdown:focus {
border-color: var(--input-border-focus) !important;
box-shadow: 0 0 0 3px var(--accent-glow), var(--shadow-md) !important;
}
/* Multi-select token chips inside dropdown */
.gradio-dropdown .token {
background: var(--chip-bg) !important;
border: 1px solid var(--chip-border) !important;
border-radius: var(--radius-pill) !important;
color: var(--chip-text) !important;
font-size: 11px !important;
padding: 2px 8px !important;
}
.whyx-artist-preview { background: var(--surface) !important; border: 1px solid var(--glass-border) !important; border-radius: var(--radius-md) !important; padding: 12px 14px !important; margin-top: 12px !important; box-shadow: var(--shadow-sm) !important; }
.whyx-artist-preview-title { color: var(--text) !important; background: none !important; -webkit-background-clip: unset !important; -webkit-text-fill-color: unset !important; background-clip: unset !important; font-weight: 600 !important; font-size: 13px !important; margin-bottom: 6px !important; }
.whyx-artist-preview-empty { color: var(--text-dim) !important; font-size: 12px !important; font-style: italic !important; }
.whyx-artist-link { display: inline-flex !important; align-items: center !important; gap: 4px !important; border: 1px solid var(--chip-border) !important; border-radius: var(--radius-sm) !important; padding: 4px 10px !important; background: var(--chip-bg) !important; color: var(--chip-text) !important; text-decoration: none !important; font-size: 11px !important; font-weight: 600 !important; transition: all var(--transition-fast) !important; }
.whyx-artist-link:hover { background: var(--accent-glow) !important; border-color: rgba(124,58,237,0.25) !important; box-shadow: 0 2px 8px rgba(79,70,229,0.08) !important; color: var(--text-accent) !important; }
.whyx-tag-bar { width: 80px !important; display: inline-block !important; white-space: nowrap !important; font-family: monospace !important; }
@media (max-width: 767px) {
.whyx-title { font-size: 24px !important; } .whyx-section-title { font-size: 11px !important; }
#root .gradio-row { flex-direction: column !important; } #root .gradio-column { width: 100% !important; min-width: 0 !important; }
.whyx-generate-btn button { font-size: 14px !important; padding: 12px 20px !important; min-height: 48px !important; }
.whyx-cat-row { flex-direction: row !important; flex-wrap: wrap !important; gap: 6px !important; }
.whyx-cat-btn { flex: 0 0 calc(33.33% - 5px) !important; min-width: 0 !important; }
.whyx-cat-btn label { font-size: 9px !important; padding: 6px 2px !important; min-height: 34px !important; white-space: normal !important; line-height: 1.1 !important; }
.whyx-cat-btn .info, .whyx-cat-btn [class*="info"] { display: none !important; }
.whyx-cat-btn label > span:first-child { display: none !important; }
.whyx-copy-btn { min-width: 44px !important; width: 44px !important; height: 38px !important; }
.whyx-result-text { font-size: 11px !important; }
.tabs { flex-wrap: nowrap !important; overflow-x: auto !important; scrollbar-width: none !important; }
.tabs::-webkit-scrollbar { display: none !important; }
.tabs button { flex-shrink: 0 !important; font-size: 11px !important; padding: 6px 10px !important; }
.whyx-tooltip-text { bottom: auto !important; top: 110% !important; }
.whyx-tooltip-text::after { top: -8px !important; border-color: transparent transparent var(--surface) transparent !important; }
}
@media (min-width: 768px) and (max-width: 1023px) {
.whyx-title { font-size: 26px !important; }
.whyx-cat-row { flex-direction: row !important; flex-wrap: wrap !important; gap: 8px !important; }
.whyx-cat-btn { flex: 0 0 calc(20% - 7px) !important; min-width: 0 !important; }
.whyx-cat-btn label { font-size: 10px !important; padding: 6px 3px !important; min-height: 36px !important; white-space: normal !important; line-height: 1.15 !important; }
.whyx-cat-btn .info, .whyx-cat-btn [class*="info"] { display: none !important; }
.whyx-cat-btn label > span:first-child { display: none !important; }
}
/* ===== Tree View UI ===== */
:root {
--tree-node-bg: rgba(14,19,32,0.88);
--tree-node-border: rgba(255,255,255,0.07);
--tree-node-border-hover: rgba(255,255,255,0.14);
--tree-connector: rgba(110,231,210,0.15);
--tree-connector-hover: rgba(110,231,210,0.35);
--tree-branch-hover: rgba(79,70,229,0.08);
--tree-leaf-hover: rgba(14,19,32,0.92);
--tree-indent: 16px;
--tree-transition: 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.whyx-tree {
display: flex !important;
flex-direction: column !important;
gap: 8px !important;
width: 100% !important;
}
.whyx-tree-node {
background: var(--tree-node-bg) !important;
border: 1px solid var(--tree-node-border) !important;
border-radius: var(--radius-md) !important;
box-shadow: var(--shadow-sm) !important;
transition: border-color var(--tree-transition), box-shadow var(--tree-transition), transform var(--tree-transition) !important;
overflow: hidden !important;
position: relative !important;
}
.whyx-tree-node:hover {
border-color: rgba(124,58,237,0.22) !important;
box-shadow: 0 6px 24px rgba(0,0,0,0.25), 0 0 0 1px rgba(124,58,237,0.08), 0 0 24px rgba(124,58,237,0.05) !important;
transform: translateY(-1px) !important;
}
.whyx-tree-branch {
border-radius: var(--radius-md) !important;
}
.whyx-tree-branch > .gradio-accordion label[class*="accordion"],
.whyx-tree-branch > .gradio-accordion button,
.whyx-tree-branch > .gradio-accordion summary {
background: var(--tree-node-bg) !important;
border-bottom: 1px solid var(--tree-node-border) !important;
color: var(--text) !important;
font-weight: 600 !important;
font-size: 12px !important;
padding: 10px 14px !important;
cursor: pointer !important;
display: flex !important;
align-items: center !important;
gap: 6px !important;
transition: background var(--tree-transition), border-color var(--tree-transition) !important;
}
.whyx-tree-branch > .gradio-accordion label[class*="accordion"]:hover,
.whyx-tree-branch > .gradio-accordion button:hover,
.whyx-tree-branch > .gradio-accordion summary:hover {
background: var(--tree-branch-hover) !important;
border-color: var(--tree-connector-hover) !important;
}
.whyx-tree-branch > .gradio-accordion > div:not(label) {
padding: 10px 14px !important;
animation: whyxFadeIn 0.25s ease both !important;
}
.whyx-tree-branch > .gradio-accordion[open] > div:not(label) {
animation: whyxFadeInDown 0.25s ease both !important;
}
@keyframes whyxFadeInDown {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
.whyx-tree-leaf {
padding: 10px 14px !important;
border-top: 1px solid var(--tree-node-border) !important;
transition: background var(--tree-transition) !important;
}
.whyx-tree-leaf:hover {
background: var(--tree-leaf-hover) !important;
}
.whyx-tree-leaf:first-child {
border-top: none !important;
}
.whyx-tree-indent {
margin-left: var(--tree-indent) !important;
}
.whyx-tree-connector {
position: absolute !important;
left: -8px !important;
top: 0 !important;
bottom: 0 !important;
width: 2px !important;
background: var(--tree-connector) !important;
border-radius: 1px !important;
}
.whyx-tree-connector:hover {
background: var(--tree-connector-hover) !important;
}
.whyx-tree-item {
display: flex !important;
align-items: center !important;
gap: 8px !important;
padding: 6px 0 !important;
}
.whyx-tree-item-icon {
font-size: 14px !important;
line-height: 1 !important;
flex-shrink: 0 !important;
color: var(--text-accent) !important;
}
.whyx-tree-item-label {
color: var(--text) !important;
font-size: 12px !important;
font-weight: 500 !important;
flex: 1 !important;
}
.whyx-tree-item-value {
color: var(--text-dim) !important;
font-size: 11px !important;
font-family: 'JetBrains Mono', ui-monospace, monospace !important;
}
.whyx-tree-divider {
height: 1px !important;
background: linear-gradient(90deg, transparent, var(--tree-connector), transparent) !important;
margin: 8px 0 !important;
border: none !important;
}
/* Tree node header (replaces _section for tree) */
.whyx-tree-header {
display: flex !important;
align-items: center !important;
gap: 10px !important;
padding: 10px 14px !important;
border-bottom: 1px solid var(--tree-node-border) !important;
animation: whyxFadeIn 0.3s ease both !important;
}
.whyx-tree-header-icon {
font-size: 18px !important;
line-height: 1.4 !important;
flex-shrink: 0 !important;
}
.whyx-tree-header-title {
color: var(--text) !important;
font-weight: 700 !important;
font-size: 11px !important;
letter-spacing: 1px !important;
text-transform: uppercase !important;
line-height: 1.4 !important;
}
.whyx-tree-header-desc {
color: var(--text-dim) !important;
font-size: 11px !important;
line-height: 1.45 !important;
margin-top: 2px !important;
max-width: 640px !important;
}
/* Tree branch expand/collapse icon */
.whyx-tree-branch > .gradio-accordion label[class*="accordion"]::before,
.whyx-tree-branch > .gradio-accordion button::before,
.whyx-tree-branch > .gradio-accordion summary::before {
content: "โธ" !important;
display: inline-block !important;
transition: transform var(--tree-transition) !important;
font-size: 11px !important;
margin-right: 4px !important;
color: var(--text-accent) !important;
}
.whyx-tree-branch > .gradio-accordion[open] > label[class*="accordion"]::before,
.whyx-tree-branch > .gradio-accordion[open] > button::before,
.whyx-tree-branch > .gradio-accordion[open] > summary::before {
transform: rotate(90deg) !important;
}
/* === Slide-down panels (inline expand/collapse โ DOM persists, animates max-height) === */
.whyx-slide-panel {
overflow: hidden !important;
max-height: 0 !important;
opacity: 0 !important;
transform: translateY(-4px) !important;
padding: 0 0 0 14px !important;
margin: 0 !important;
border-left: 2px solid rgba(124,58,237,0.12) !important;
transition: max-height .25s ease-out, opacity .2s ease-out .05s, transform .25s ease-out, padding .2s, margin .2s !important;
background: linear-gradient(180deg, rgba(124,58,237,0.05), rgba(79,70,229,0.02) 60%, transparent) !important;
border-radius: 0 8px 8px 0 !important;
}
.whyx-slide-open {
max-height: 2200px !important;
opacity: 1 !important;
transform: translateY(0) !important;
padding: 4px 0 8px 14px !important;
margin-top: 4px !important;
}
/* Soft trigger button โ pill, glass surface, glow on hover */
.whyx-slide-trigger {
display: inline-flex !important;
align-items: center !important;
gap: 6px !important;
padding: 7px 16px !important;
border: 1px solid rgba(255,255,255,0.08) !important;
background: linear-gradient(135deg, rgba(20,28,46,0.6), rgba(15,23,42,0.4)) !important;
border-radius: 999px !important;
color: var(--text-dim, #94A3B8) !important;
font-size: 12px !important;
font-weight: 600 !important;
cursor: pointer !important;
user-select: none !important;
box-shadow: var(--shadow-soft, 0 2px 8px rgba(0,0,0,0.15)), inset 0 1px 0 rgba(255,255,255,0.03) !important;
transition: all var(--transition-fast, 0.15s) !important;
position: relative !important;
}
.whyx-slide-trigger:hover {
border-color: rgba(124,58,237,0.35) !important;
color: var(--text, #E2E8F0) !important;
box-shadow: 0 4px 18px rgba(124,58,237,0.15), inset 0 1px 0 rgba(255,255,255,0.05) !important;
transform: translateY(-1px) !important;
}
.whyx-slide-trigger.whyx-slide-active {
color: var(--text, #E2E8F0) !important;
border-color: rgba(124,58,237,0.3) !important;
background: linear-gradient(135deg, rgba(124,58,237,0.12), rgba(79,70,229,0.08)) !important;
box-shadow: 0 4px 16px rgba(124,58,237,0.18) !important;
}
/* Soft glow accent ring for tree nodes */
.whyx-tree-node {
box-shadow: var(--shadow-soft, 0 2px 8px rgba(0,0,0,0.15), 0 8px 24px rgba(79,70,229,0.06)), inset 0 1px 0 rgba(255,255,255,0.03) !important;
}
"""
def _tree_header(icon: str, title_key: str, lang: str = "en", desc_key: str | None = None) -> str:
desc = ""
if desc_key:
desc = f''
return f"""
"""
def _user_preset_choices(lang: str = "en"):
names = get_user_preset_names()
return [(t("user_preset_pick", lang), "")] + [(n, n) for n in names]
_blocks_kw = {"title": "Whyx-PROmpTea"}
if _GRADIO_MAJOR < 6:
_blocks_kw["theme"] = WhyxTheme()
_blocks_kw["css"] = WHYX_CSS
_blocks_kw["head"] = f""
with gr.Blocks(**_blocks_kw) as demo:
lang_state = gr.State("EN")
with gr.Row():
with gr.Column(scale=1, min_width=0):
title_html = gr.HTML(f"""
Whyx-PROmpTea
{t("app_subtitle", "en")}
""")
with gr.Column(scale=0, min_width=230):
with gr.Row(equal_height=True):
theme_toggle = gr.Radio(
choices=[("๐ Dark", "dark"), ("โ๏ธ Light", "light")],
value="dark",
label="",
show_label=False,
interactive=True,
container=False,
elem_classes=["whyx-theme-btn", "whyx-radio-group"],
)
lang_btn = gr.Radio(
choices=["EN", "RU"],
value="EN",
label="",
show_label=False,
interactive=True,
container=False,
elem_classes=["whyx-lang-btn", "whyx-radio-group"],
)
gr.HTML('
')
current_preset_state = gr.State("")
with gr.Tabs():
# ===== TAB 1: Generate =====
with gr.TabItem(f"โจ {t('tab_generate', 'en')}"):
with gr.Column(elem_classes=["whyx-tree"]):
# Tree root: Prompt
with gr.Column(elem_classes=["whyx-tree-node"]):
prompt_input = gr.Textbox(
label="",
placeholder=t("input_placeholder", "en"),
lines=5,
max_lines=10,
elem_classes=["whyx-field"],
)
# Tree branch: Model & Rating (slide-down)
with gr.Column(elem_classes=["whyx-tree-node"]):
gr.HTML(_tree_header("โ๏ธ", "model_label", "en", "model_desc"))
with gr.Row():
model_trigger = gr.Button(
f'โธ {t("model_label", "en")}',
elem_classes=["whyx-slide-trigger"], elem_id="whyx-trig-model",
)
rating_trigger = gr.Button(
f'โธ {t("rating_label", "en")}',
elem_classes=["whyx-slide-trigger"], elem_id="whyx-trig-rating",
)
model_dropdown = gr.Radio(
choices=[(t("anima", "en"), "anima"), (t("illustrious", "en"), "illustrious")],
value="anima", label="", show_label=False,
elem_id="whyx-panel-model",
elem_classes=["whyx-slide-panel", "whyx-slide-closed", "whyx-chip-group"],
)
rating_dropdown = gr.Radio(
choices=[
(t("rating_pg", "en"), "pg"),
(t("rating_pg13", "en"), "pg13"),
(t("rating_pg16", "en"), "pg16"),
(t("rating_r", "en"), "r"),
(t("rating_rplus", "en"), "r+"),
],
value="pg", label="", show_label=False,
elem_id="whyx-panel-rating",
elem_classes=["whyx-slide-panel", "whyx-slide-closed", "whyx-chip-group"],
)
# Tree branch: Variations
with gr.Accordion(f"๐ข {t('variations_label', 'en')}", open=True, elem_classes=["whyx-tree-branch"]):
with gr.Row():
num_variations = gr.Slider(
minimum=1,
maximum=10,
value=5,
step=1,
label=t("variations_label", "en"),
info=t("variations_desc", "en"),
elem_classes=["whyx-field"],
)
seed_input = gr.Number(
value=None,
label=t("seed_label", "en"),
info=t("seed_info", "en"),
precision=0,
minimum=-1,
elem_classes=["whyx-field"],
)
# Tree branch: Creativity
with gr.Accordion(f"๐จ {t('creativity_label', 'en')}", open=True, elem_classes=["whyx-tree-branch"]):
with gr.Row():
creativity = gr.Radio(
choices=[
(t("creativity_very_low", "en"), "very_low"),
(t("creativity_low", "en"), "low"),
(t("creativity_medium", "en"), "medium"),
(t("creativity_high", "en"), "high"),
(t("creativity_very_high", "en"), "very_high"),
(t("creativity_extreme", "en"), "extreme"),
],
value="medium",
label=t("creativity_label", "en"),
info=t("creativity_desc", "en"),
elem_classes=["whyx-radio-group", "whyx-field"],
)
weight_mode = gr.Radio(
choices=[
(t("weight_off", "en"), "off"),
(t("weight_light", "en"), "light"),
(t("weight_on", "en"), "on"),
],
value="off",
label=t("weight_label", "en"),
info=t("weight_desc", "en"),
elem_classes=["whyx-radio-group", "whyx-field"],
)
# Tree branch: Mode / Web / FX
with gr.Accordion(f"โก {t('mode_label', 'en')}", open=True, elem_classes=["whyx-tree-branch"]):
with gr.Row():
mode_toggle = gr.Radio(
choices=[
(t("mode_standard", "en"), "standard"),
(t("mode_rewrite", "en"), "rewrite"),
],
value="standard",
label=t("mode_label", "en"),
info=t("mode_desc", "en"),
elem_classes=["whyx-radio-group", "whyx-field"],
)
web_enrich_toggle = gr.Radio(
choices=[
(t("web_enrich_off", "en"), "0"),
(t("web_enrich_on", "en"), "1"),
],
value="0",
label=t("web_enrich_label", "en"),
info=t("web_enrich_desc", "en"),
elem_classes=["whyx-radio-group", "whyx-field"],
)
fx_toggle = gr.Radio(
choices=[
(t("fx_off", "en"), "off"),
(t("fx_light", "en"), "light"),
(t("fx_rich", "en"), "rich"),
],
value="light",
label=t("fx_label", "en"),
info=t("fx_desc", "en"),
elem_classes=["whyx-radio-group", "whyx-field"],
)
# Tree root: Advanced
with gr.Column(elem_classes=["whyx-tree-node"]):
adv_header_html = gr.HTML(_tree_header("๐งน", "advanced_label", "en", "advanced_desc"))
output_format_radio = gr.Radio(
choices=[
(t("output_format_prompt", "en"), "prompt"),
(t("output_format_booru", "en"), "booru"),
],
value="prompt",
label=t("output_format_label", "en"),
info=t("output_format_desc", "en"),
elem_classes=["whyx-radio-group"],
)
cleanup_acc = gr.Accordion(label="โพ " + t("cleanup_label", "en"), open=False, elem_classes=["whyx-tree-branch"])
with cleanup_acc:
blacklist_box = gr.Textbox(
label=t("blacklist_label", "en"),
info=t("blacklist_desc", "en"),
placeholder=t("blacklist_ph", "en"),
lines=2,
)
mirror_blacklist = gr.Checkbox(label=t("blacklist_mirror", "en"), value=False)
with gr.Row(elem_classes=["whyx-strip-row"]):
strip_quality = gr.Checkbox(label=t("strip_quality", "en"), value=False)
strip_artist = gr.Checkbox(label=t("strip_artist", "en"), value=False)
strip_lora = gr.Checkbox(label=t("strip_lora", "en"), value=False)
strip_meta = gr.Checkbox(label=t("strip_meta", "en"), value=False)
min_tags = gr.Slider(
minimum=0, maximum=40, step=1, value=0,
label=t("min_tags_label", "en"), info=t("min_tags_desc", "en"),
)
presets_acc = gr.Accordion(label="โพ " + t("user_presets_label", "en"), open=False, elem_classes=["whyx-tree-branch"])
with presets_acc:
user_preset_name = gr.Textbox(
label=t("user_preset_name", "en"), placeholder=t("user_preset_name_ph", "en"),
)
user_preset_tags = gr.Textbox(
label=t("user_preset_tags", "en"), placeholder=t("user_preset_tags_ph", "en"), lines=2,
)
with gr.Row():
user_preset_save = gr.Button(t("user_preset_save", "en"), variant="primary")
user_preset_delete = gr.Button(t("user_preset_delete", "en"))
user_preset_trigger = gr.Button(
f'โธ {t("user_preset_select", "en")}',
elem_classes=["whyx-slide-trigger"], elem_id="whyx-trig-user-preset",
)
user_preset_dropdown = gr.Radio(
choices=_user_preset_choices("en"), value="",
label="", show_label=False,
elem_id="whyx-panel-user-preset",
elem_classes=["whyx-slide-panel", "whyx-slide-closed", "whyx-chip-group"],
)
user_preset_apply = gr.Button(t("user_preset_apply", "en"))
user_preset_status = gr.Markdown(visible=True)
user_preset_state = gr.State("")
# Tree root: Categories
with gr.Column(elem_classes=["whyx-tree-node"]):
categories_header_html = gr.HTML(_tree_header("๐จ", "categories_label", "en"))
category_checks = []
cat_names = []
for cat in ALL_CATEGORIES:
cat_name = t(cat, "en")
icon = CATEGORY_ICONS.get(cat, "๐")
cat_desc_key = f"{cat}_desc"
cat_names.append((cat, cat_name, icon, cat_desc_key))
for row_start, row_size in [(0, 7), (7, 7), (14, 7), (21, 7)]:
with gr.Row(elem_classes=["whyx-cat-row"]):
for cat, cat_name, icon, cat_desc_key in cat_names[row_start:row_start + row_size]:
with gr.Column(scale=1, min_width=100):
cb = gr.Checkbox(
label=f"{icon} {cat_name}",
info=t(cat_desc_key, "en"),
value=cat in DEFAULT_CHECKED,
elem_classes=["whyx-cat-btn"],
)
category_checks.append(cb)
categories_desc_md = gr.Markdown(
f'{t("categories_desc", "en")}
',
visible=True,
)
# Tree root: Presets
with gr.Column(elem_classes=["whyx-tree-node"]):
presets_header_html = gr.HTML(_tree_header("โก", "presets_label", "en"))
presets_desc_md = gr.Markdown(
f'{t("presets_desc", "en")}
',
visible=True,
)
preset_groups = {}
for group in PRESET_GROUPS:
gr.Markdown(
f'{group["icon"]} {group["label_en"]}
'
)
choices = [
(f"{PRESETS[p]["icon"]} {PRESETS[p]["label_en"]}", p)
for p in group["presets"]
]
cg = gr.CheckboxGroup(
choices=choices,
label="",
value=[],
elem_classes=["whyx-preset-toggle"],
elem_id=f"preset-group-{group["key"]}",
)
preset_groups[group["key"]] = cg
generate_btn = gr.Button(
t("generate_btn", "en"),
variant="primary",
size="lg",
elem_classes=["whyx-generate-btn"],
)
with gr.Row():
suggest_btn = gr.Button(
t("suggest_btn", "en"),
variant="secondary",
size="sm",
elem_classes=["whyx-suggest-btn"],
)
suggest_output = gr.HTML(visible=True)
hotkeys_hint_md = gr.Markdown(
f'{t("hotkeys_hint", "en")}
',
visible=True,
)
# Tree root: Results
with gr.Column(elem_classes=["whyx-tree-node"]):
results_header_html = gr.HTML(_tree_header("๐", "results_label", "en"))
reload_btn = gr.Button(t("reload_btn", "en"), variant="secondary", size="sm")
results_desc_md = gr.Markdown(
value=f'{t("results_desc", "en")}
',
)
result_boxes = []
neg_boxes = []
heart_btns = []
for i in range(MAX_OUTPUTS):
with gr.Row(elem_classes=["whyx-result-card"]) as card:
with gr.Column(scale=0, min_width=40):
gr.HTML(f'#{i+1}
')
with gr.Column(scale=1):
rb = gr.Textbox(
label="",
show_label=False,
lines=3,
max_lines=8,
interactive=False,
placeholder=t("result_placeholder", "en"),
elem_classes=["whyx-result-text"],
)
with gr.Column(scale=0, min_width=36):
cb_pos = gr.Button(
"๐",
size="sm",
variant="secondary",
scale=0,
min_width=32,
elem_classes=["whyx-copy-btn", "whyx-tooltip"],
elem_id="copy-btn-pos",
)
cb_pos.click(fn=None, inputs=[rb], outputs=[], js=COPY_JS)
gr.HTML('Copy to clipboard')
with gr.Column(scale=0, min_width=46):
hb = gr.Button(
"โก",
size="sm",
variant="secondary",
scale=0,
min_width=36,
elem_classes=["whyx-heart-btn", "whyx-tooltip"],
elem_id="heart-btn",
)
heart_btns.append(hb)
gr.HTML('Toggle favorite')
with gr.Column(scale=1):
nb = gr.Textbox(
label="",
show_label=False,
lines=2,
max_lines=4,
interactive=False,
placeholder=t("neg_placeholder", "en"),
elem_classes=["whyx-neg-text"],
)
with gr.Column(scale=0, min_width=36):
cb_neg = gr.Button(
"๐",
size="sm",
variant="secondary",
scale=0,
min_width=32,
elem_classes=["whyx-copy-btn", "whyx-tooltip"],
elem_id="copy-btn-neg",
)
cb_neg.click(fn=None, inputs=[nb], outputs=[], js=COPY_JS)
gr.HTML('Copy to clipboard')
result_boxes.append(rb)
neg_boxes.append(nb)
# ===== TAB 2: Tagger =====
with gr.TabItem(f"๐ผ๏ธ {t('tab_tagger', 'en')}"):
with gr.Column(elem_classes=["whyx-tree"]):
# Tree root: Tagger
with gr.Column(elem_classes=["whyx-tree-node"]):
tagger_header_html = gr.HTML(_tree_header("๐ผ๏ธ", "tagger_section", "en"))
with gr.Row():
with gr.Column(scale=1):
tagger_image = gr.Image(
sources=["upload"], type="numpy", label=t("tagger_image", "en"),
height=340, elem_classes=["whyx-tagger-image"],
)
tagger_gen_threshold = gr.Slider(
minimum=0.1, maximum=0.9, value=0.35, step=0.05,
label=t("tagger_gen_threshold", "en"),
info=t("tagger_gen_threshold_desc", "en"),
)
tagger_char_threshold = gr.Slider(
minimum=0.5, maximum=1.0, value=0.75, step=0.05,
label=t("tagger_char_threshold", "en"),
info=t("tagger_char_threshold_desc", "en"),
)
tagger_btn = gr.Button(
t("tagger_btn", "en"), variant="primary", size="lg",
elem_classes=["whyx-tagger-run-btn"],
)
with gr.Column(scale=1):
# Slide-panel pattern, same as Generate tab.
with gr.Row():
tagger_mode_trigger = gr.Button(
f'โธ {t("tagger_model_label", "en")}',
elem_classes=["whyx-slide-trigger"], elem_id="whyx-trig-tagger-mode",
)
tagger_mode = gr.Radio(
choices=[
(t("tagger_mode_smart", "en"), "ensemble"),
(t("tagger_mode_eva_large", "en"), "wd:eva02"),
(t("tagger_mode_dd", "en"), "deepdanbooru"),
(t("tagger_mode_florence", "en"), "nl"),
(t("tagger_mode_qwen", "en"), "qwen"),
],
value="ensemble",
label="",
show_label=False,
elem_id="whyx-panel-tagger-mode",
elem_classes=["whyx-slide-panel", "whyx-slide-closed", "whyx-chip-group"],
)
# Single "Smart analysis" toggle โ replaces the old
# separate Pose analysis / Pose method / Smart analysis
# controls. ON (default): Whole-body DWPose detects
# pose + anatomy + expression + background. OFF: only
# base WD14 tags (ratings / characters / general).
tagger_smart_toggle = gr.Checkbox(
value=True,
label=t("tagger_smart_toggle_label", "en"),
info=t("tagger_smart_toggle_info", "en"),
elem_classes=["whyx-checkbox"],
)
# Optional NL caption โ OFF by default (slow on CPU).
tagger_caption_toggle = gr.Checkbox(
value=False,
label=t("tagger_caption_toggle_label", "en"),
info=t("tagger_caption_toggle_info", "en"),
elem_classes=["whyx-checkbox"],
)
# ControlNet-style preview QA for the pose pass
# (pure numpy/PIL โ no extra models). Rendered
# from the same keypoints the pose tags came from.
with gr.Row():
tagger_control_trigger = gr.Button(
f'โธ {t("tagger_control_label", "en")}',
elem_classes=["whyx-slide-trigger"], elem_id="whyx-trig-tagger-control",
)
tagger_control_kind = gr.Radio(
choices=[
(t("tagger_control_skeleton_black", "en"), "skeleton_black"),
(t("tagger_control_skeleton_overlay", "en"), "skeleton_overlay"),
(t("tagger_control_canny", "en"), "canny"),
(t("tagger_control_depth", "en"), "depth"),
(t("tagger_control_segmentation", "en"), "segmentation"),
],
value="skeleton_black",
label="",
show_label=False,
elem_id="whyx-panel-tagger-control",
elem_classes=["whyx-slide-panel", "whyx-slide-closed", "whyx-chip-group"],
)
tagger_control_img = gr.Image(
label=t("tagger_control_img_label", "en"),
interactive=False, visible=False,
elem_classes=["whyx-tagger-control-img"],
)
tagger_control_dl = gr.DownloadButton(
"โฌ PNG", visible=False, size="sm",
)
tagger_format = gr.Radio(
choices=[
(t("tagger_format_prompt", "en"), "prompt"),
(t("tagger_format_raw", "en"), "raw"),
],
value="prompt",
label=t("tagger_format_label", "en"),
info=t("tagger_format_desc", "en"),
elem_classes=["whyx-radio-group"],
)
tagger_output = gr.HTML(
value=f'{t("tagger_desc", "en")}
',
)
with gr.Row():
tagger_apply_btn = gr.Button(
t("tagger_apply_btn", "en"), variant="primary", size="lg",
visible=False, elem_classes=["whyx-tagger-apply-btn"],
)
tagger_copy_btn = gr.Button(
"๐ " + t("tagger_copy", "en"), size="lg",
visible=False, elem_classes=["whyx-tagger-copy-btn"],
)
# Tree branch: Results
with gr.Accordion(f"๐ท๏ธ {t('tagger_chips_label', 'en')}", open=False, elem_classes=["whyx-tree-branch"]):
tagger_chips = gr.CheckboxGroup(
choices=[], value=[], label="",
elem_classes=["whyx-tagger-chips"], visible=False,
)
tagger_chars = gr.CheckboxGroup(
choices=[], value=[], label="",
elem_classes=["whyx-tagger-chips"], visible=False,
)
tagger_tags_box = gr.Textbox(
label=t("tagger_tags_label", "en"),
info=t("tagger_tags_desc", "en"),
lines=5, value="",
placeholder=t("tagger_tags_ph", "en"),
elem_classes=["whyx-tagger-tags-box"], visible=False,
)
tagger_raw_state = gr.State("")
tagger_esc_state = gr.State("")
# ===== TAB 3: Tools =====
with gr.TabItem(f"๐ง {t('tab_tools', 'en')}"):
with gr.Column(elem_classes=["whyx-tree"]):
# Tree root: Analyzer
with gr.Column(elem_classes=["whyx-tree-node"]):
analyzer_header_html = gr.HTML(_tree_header("๐", "analyzer_section", "en"))
analyzer_md = gr.Markdown(
value=f'{t("analyzer_desc", "en")}
',
visible=True,
)
# Tree root: Artist (slide-down)
with gr.Column(elem_classes=["whyx-tree-node"]):
artist_header_html = gr.HTML(_tree_header("๐๏ธ", "artist_section", "en"))
with gr.Row():
artist_style_trigger = gr.Button(
f'โธ {t("artist_style_placeholder", "en")}',
elem_classes=["whyx-slide-trigger"], elem_id="whyx-trig-artist-style",
)
artist_search_trigger = gr.Button(
f'โธ {t("artist_search_label", "en")}',
elem_classes=["whyx-slide-trigger"], elem_id="whyx-trig-artist-search",
)
artist_multiselect_trigger = gr.Button(
f'โธ {t("artist_select", "en")}',
elem_classes=["whyx-slide-trigger"], elem_id="whyx-trig-artist-multiselect",
)
artist_style_dropdown = gr.Radio(
choices=STYLECHOICES, value="",
label="", show_label=False,
elem_id="whyx-panel-artist-style",
elem_classes=["whyx-slide-panel", "whyx-slide-closed", "whyx-chip-group"],
)
artist_search = gr.Textbox(
label=t("artist_search_label", "en"),
placeholder=t("artist_search_placeholder", "en"),
info=t("artist_search_desc", "en"),
elem_id="whyx-panel-artist-search",
elem_classes=["whyx-slide-panel", "whyx-slide-closed"],
)
artist_multiselect = gr.CheckboxGroup(
choices=ARTISTCHOICES, value=[],
label="", show_label=False,
elem_id="whyx-panel-artist-multiselect",
elem_classes=["whyx-slide-panel", "whyx-slide-closed", "whyx-chip-group"],
)
artist_style_dropdown.change(
fn=on_artist_filter_change,
inputs=[artist_style_dropdown, artist_search],
outputs=[artist_multiselect],
)
artist_search.change(
fn=on_artist_filter_change,
inputs=[artist_style_dropdown, artist_search],
outputs=[artist_multiselect],
)
artist_info_md = gr.HTML(
value=_format_artist_info([], "en"),
visible=True,
)
artist_multiselect.change(
fn=on_artist_selection_change,
inputs=[artist_multiselect, lang_state],
outputs=[artist_info_md],
)
artist_style_dropdown.change(
fn=on_artist_style_recommendations,
inputs=[artist_style_dropdown, lang_state],
outputs=[artist_info_md],
)
# Tree root: Tandem (slide-down)
with gr.Column(elem_classes=["whyx-tree-node"]):
tandem_header_html = gr.HTML(_tree_header("๐ค", "tandem_section", "en"))
with gr.Row():
use_tandems = gr.Checkbox(
label=t("tandem_random", "en"),
info=t("tandem_random_desc", "en"),
value=False,
scale=0,
min_width=140,
)
tandem_trigger = gr.Button(
f'โธ {t("tandem_select", "en")}',
elem_classes=["whyx-slide-trigger"], elem_id="whyx-trig-tandem",
scale=2,
)
tandem_dropdown = gr.Radio(
choices=TANDEMCHOICES, value=None,
label="", show_label=False,
elem_id="whyx-panel-tandem",
elem_classes=["whyx-slide-panel", "whyx-slide-closed", "whyx-chip-group"],
)
# Tree root: Web Search
with gr.Column(elem_classes=["whyx-tree-node"]):
web_search_panel = gr.Accordion(t("web_search_label", "en"), open=False, elem_classes=["whyx-tree-branch"])
with web_search_panel:
web_search_input = gr.Textbox(
label="", show_label=False,
placeholder=t("web_search_placeholder", "en"),
scale=1,
)
with gr.Row():
web_search_btn = gr.Button(t("web_search_btn", "en"), scale=0, variant="secondary")
web_search_clear = gr.ClearButton(value="โ", scale=0, variant="secondary")
web_search_output = gr.HTML(
value=f'{t("web_search_empty", "en")}
',
)
# ===== TAB 4: History =====
with gr.TabItem(f"๐ {t('tab_history', 'en')}"):
with gr.Column(elem_classes=["whyx-tree-node"]):
history_header_html = gr.HTML(_tree_header("๐", "history_section", "en"))
history_md = gr.HTML(
value=_format_history_html("en"),
visible=True,
)
web_search_btn.click(
fn=on_web_search,
inputs=[web_search_input, lang_state],
outputs=[web_search_output],
)
web_search_input.submit(
fn=on_web_search,
inputs=[web_search_input, lang_state],
outputs=[web_search_output],
)
reload_btn.click(
fn=_reload_data,
inputs=[lang_state],
outputs=[],
)
web_search_clear.click(
fn=lambda lang: f'{t("web_search_empty", "ru" if lang == "RU" else "en")}
',
inputs=[lang_state],
outputs=[web_search_output],
)
for i, hb in enumerate(heart_btns):
hb.click(
fn=lambda idx=i, *args: _heart_click(idx, *args),
inputs=[lang_state] + result_boxes,
outputs=[history_md] + heart_btns,
)
# Fold ensemble `mode` into the tagger call while keeping fmt semantics.
# "nl"/"qwen" are caption-only modes: WD14 / pose / rating passes are
# skipped entirely and the caption is the sole output (see on_tag_nl_only).
def on_tag_image_with_mode(image, gen_threshold, char_threshold, fmt, mode, caption_toggle, smart_toggle, lang, control_kind="skeleton_black", progress=gr.Progress()):
if mode in ("nl", "qwen"):
caption_kind = "qwen" if mode == "qwen" else "florence"
return on_tag_nl_only(image, fmt, lang, caption_kind, progress=progress)
mode_key = mode if isinstance(mode, str) and mode.startswith(
("wd:", "deepdanbooru", "ensemble")
) else "ensemble"
effective_fmt = f"{fmt}|{mode_key}" if mode_key != "ensemble" else fmt
# Smart analysis is the single switch for the full pipeline: when ON the
# backend auto-selects Whole-body DWPose (pose + anatomy + expression +
# background). When OFF we skip pose entirely and return base WD14 tags.
smart_analysis = bool(smart_toggle)
skip_pose = not smart_analysis
pose_mode = "wholebody" if smart_analysis else "yolo"
# The caption checkbox is the single source of truth for the NL
# caption; the "nl"/"qwen" radio items only check the box (see
# on_tag_rerun). "qwen" selects the Qwen2.5-VL-3B captioner
# (Anima-style descriptions), everything else uses Florence-2-base.
with_caption = bool(caption_toggle)
caption_kind = "qwen" if mode == "qwen" else "florence"
return on_tag_image(image, gen_threshold, char_threshold, effective_fmt, lang, progress=progress, skip_pose=skip_pose, with_caption=with_caption, caption_kind=caption_kind, control_kind=control_kind, pose_mode=pose_mode, smart_analysis=smart_analysis)
tagger_outputs = [
tagger_output, tagger_apply_btn, tagger_chips, tagger_chars,
tagger_tags_box, tagger_copy_btn, tagger_raw_state, tagger_esc_state,
tagger_control_img, tagger_control_dl,
]
tagger_btn.click(
fn=on_tag_image_with_mode,
inputs=[tagger_image, tagger_gen_threshold, tagger_char_threshold, tagger_format, tagger_mode, tagger_caption_toggle, tagger_smart_toggle, lang_state, tagger_control_kind],
outputs=tagger_outputs,
)
# The "Florence NL Caption" radio item โ the caption checkbox are synced:
# picking the radio item checks the box (inside on_tag_rerun below, in
# the same event as the caption run); unchecking snaps back to Smart.
tagger_caption_toggle.change(
fn=lambda checked: gr.update(value="ensemble") if not checked else gr.update(),
inputs=[tagger_caption_toggle],
outputs=[tagger_mode],
)
# Auto re-run when the model or the pose toggle changes (never for the
# caption checkbox โ captioning is opt-in and slow). No-op without an
# image. Selecting the "nl"/"qwen" radio item checks the caption box and
# runs the caption in the same event, so the checkbox stays the visible
# source of truth. The component outputs are deliberately NOT read back as
# inputs here: in Gradio 6, CheckboxGroup.preprocess validates the value
# against the current choices and raises on stale selections, which
# crashed re-runs ("Value: ... is not in the list of choices").
def on_tag_rerun(image, gen_threshold, char_threshold, fmt, mode, caption_toggle, smart_toggle, lang, control_kind="skeleton_black", progress=gr.Progress()):
with_caption = bool(caption_toggle) or mode in ("nl", "qwen")
caption_update = gr.update(value=True) if mode in ("nl", "qwen") else gr.skip()
if image is None:
return (gr.skip(),) * 10 + (caption_update,)
return tuple(on_tag_image_with_mode(image, gen_threshold, char_threshold, fmt, mode, with_caption, smart_toggle, lang, control_kind=control_kind, progress=progress)) + (caption_update,)
rerun_inputs = [
tagger_image, tagger_gen_threshold, tagger_char_threshold, tagger_format,
tagger_mode, tagger_caption_toggle, tagger_smart_toggle, lang_state,
tagger_control_kind,
]
tagger_mode.change(
fn=on_tag_rerun,
inputs=rerun_inputs,
outputs=tagger_outputs + [tagger_caption_toggle],
)
tagger_control_kind.change(
fn=on_tag_rerun,
inputs=rerun_inputs,
outputs=tagger_outputs + [tagger_caption_toggle],
)
# Disable the smart-analysis toggle when a non-ensemble mode is selected
# (smart analysis only applies to the Smart tagger).
tagger_mode.change(
fn=lambda m: gr.update(interactive=(m in ("ensemble", None, ""))),
inputs=[tagger_mode],
outputs=[tagger_smart_toggle],
)
tagger_chips.change(
fn=on_tagger_selection_change,
inputs=[tagger_chips, tagger_chars, tagger_format, tagger_raw_state, tagger_esc_state],
outputs=[tagger_tags_box],
)
tagger_chars.change(
fn=on_tagger_selection_change,
inputs=[tagger_chips, tagger_chars, tagger_format, tagger_raw_state, tagger_esc_state],
outputs=[tagger_tags_box],
)
tagger_format.change(
fn=on_tagger_format_change,
inputs=[tagger_format, tagger_chips, tagger_chars],
outputs=[tagger_tags_box],
)
tagger_apply_btn.click(
fn=on_tagger_apply_tags,
inputs=[prompt_input, tagger_tags_box],
outputs=[prompt_input],
)
tagger_copy_btn.click(
fn=None,
inputs=[tagger_tags_box],
outputs=[],
js="(txt) => { try { navigator.clipboard.writeText(txt); } catch(e) {} }",
)
user_preset_save.click(
fn=on_user_preset_save,
inputs=[user_preset_name, user_preset_tags, lang_state],
outputs=[user_preset_dropdown, user_preset_tags, user_preset_status],
)
user_preset_apply.click(
fn=on_user_preset_apply,
inputs=[user_preset_dropdown, current_preset_state, lang_state],
outputs=[current_preset_state, user_preset_status],
)
user_preset_delete.click(
fn=on_user_preset_delete,
inputs=[user_preset_dropdown, lang_state],
outputs=[user_preset_dropdown, user_preset_status],
)
inputs_list = [
prompt_input, model_dropdown, rating_dropdown,
num_variations, creativity, weight_mode, mode_toggle, lang_state,
artist_style_dropdown, artist_multiselect,
use_tandems, tandem_dropdown, web_enrich_toggle,
seed_input, current_preset_state, fx_toggle,
blacklist_box, mirror_blacklist, strip_quality, strip_artist, strip_lora, strip_meta, min_tags, user_preset_state,
output_format_radio,
] + category_checks
prompt_input.change(
fn=on_prompt_analyze,
inputs=[prompt_input, lang_state],
outputs=[analyzer_md],
)
generate_btn.click(
fn=on_generate,
inputs=inputs_list,
outputs=result_boxes + neg_boxes,
).then(
fn=lambda lang: gr.update(value=_format_history_html("ru" if lang == "RU" else "en")),
inputs=[lang_state],
outputs=[history_md],
)
suggest_btn.click(
fn=on_suggest,
inputs=[prompt_input, model_dropdown, rating_dropdown, lang_state],
outputs=[suggest_output],
)
group_components = [preset_groups[g["key"]] for g in PRESET_GROUPS]
for group_key, cg in preset_groups.items():
def make_handler():
def handler(prompt, *args):
return on_preset_change(prompt, *args)
return handler
cg.change(
fn=make_handler(),
inputs=[prompt_input] + group_components + [current_preset_state],
outputs=category_checks + [prompt_input, current_preset_state] + group_components,
)
rating_dropdown.change(
fn=on_rating_change,
inputs=[rating_dropdown] + category_checks,
outputs=category_checks,
)
def update_lang(choice):
lc = "ru" if choice == "RU" else "en"
updates = [
gr.update(value=f"""
Whyx-PROmpTea
{t("app_subtitle", lc)}
"""),
gr.update(value=_tree_header("๐จ", "categories_label", lc)),
gr.update(value=_tree_header("๐", "analyzer_section", lc)),
gr.update(value=_tree_header("๐๏ธ", "artist_section", lc)),
gr.update(value=_tree_header("๐ค", "tandem_section", lc)),
gr.update(value=_tree_header("โก", "presets_label", lc)),
gr.update(value=_tree_header("๐", "results_label", lc)),
gr.update(value=_tree_header("๐", "history_section", lc)),
gr.update(label=t("input_label", lc), placeholder=t("input_placeholder", lc), info=t("input_desc", lc)),
gr.update(value=f'โธ {t("model_label", lc)}'),
gr.update(
choices=[(t("anima", lc), "anima"), (t("illustrious", lc), "illustrious")],
),
gr.update(value=f'โธ {t("rating_label", lc)}'),
gr.update(
choices=[
(t("rating_pg", lc), "pg"),
(t("rating_pg13", lc), "pg13"),
(t("rating_pg16", lc), "pg16"),
(t("rating_r", lc), "r"),
(t("rating_rplus", lc), "r+"),
],
),
gr.update(label=t("variations_label", lc), info=t("variations_desc", lc)),
gr.update(
choices=[
(t("creativity_very_low", lc), "very_low"),
(t("creativity_low", lc), "low"),
(t("creativity_medium", lc), "medium"),
(t("creativity_high", lc), "high"),
(t("creativity_very_high", lc), "very_high"),
(t("creativity_extreme", lc), "extreme"),
],
label=t("creativity_label", lc),
info=t("creativity_desc", lc),
),
gr.update(
choices=[
(t("weight_off", lc), "off"),
(t("weight_light", lc), "light"),
(t("weight_on", lc), "on"),
],
label=t("weight_label", lc),
info=t("weight_desc", lc),
),
gr.update(
choices=[
(t("mode_standard", lc), "standard"),
(t("mode_rewrite", lc), "rewrite"),
],
label=t("mode_label", lc),
info=t("mode_desc", lc),
),
gr.update(
choices=[
(t("web_enrich_off", lc), "0"),
(t("web_enrich_on", lc), "1"),
],
label=t("web_enrich_label", lc),
info=t("web_enrich_desc", lc),
),
gr.update(
choices=[
(t("fx_off", lc), "off"),
(t("fx_light", lc), "light"),
(t("fx_rich", lc), "rich"),
],
label=t("fx_label", lc),
info=t("fx_desc", lc),
),
gr.update(value=t("generate_btn", lc)),
gr.update(label=t("web_search_label", lc)),
gr.update(placeholder=t("web_search_placeholder", lc)),
gr.update(value=t("web_search_btn", lc)),
gr.update(value=f'โธ {t("artist_style_placeholder", lc)}'),
gr.update(
choices=[(s.title() if s else t("artist_all", lc), s) for s in artist_styles],
),
gr.update(label=t("artist_search_label", lc), placeholder=t("artist_search_placeholder", lc), info=t("artist_search_desc", lc)),
gr.update(value=f'โธ {t("artist_select", lc)}'),
gr.update(choices=ARTISTCHOICES),
gr.update(label=t("tandem_random", lc), info=t("tandem_random_desc", lc)),
gr.update(value=f'โธ {t("tandem_select", lc)}'),
gr.update(
choices=[(t("tandem_placeholder", lc), None)] + [
(
f"๐ค {tand['artists'][0]} ร {tand['artists'][1]} [{tand.get('style_blend', '')}] ({tand.get('compatibility', 0):.0%})",
i,
)
for i, tand in enumerate(all_tandems)
],
),
gr.update(label=t("seed_label", lc), info=t("seed_info", lc)),
gr.update(value=t("reload_btn", lc)),
gr.update(value=_tree_header("๐ผ๏ธ", "tagger_section", lc)),
gr.update(label=t("tagger_image", lc)),
gr.update(label=t("tagger_gen_threshold", lc), info=t("tagger_gen_threshold_desc", lc)),
gr.update(label=t("tagger_char_threshold", lc), info=t("tagger_char_threshold_desc", lc)),
gr.update(value=t("tagger_btn", lc)),
gr.update(value=t("tagger_apply_btn", lc)),
gr.update(value=f'{t("tagger_desc", lc)}
'),
gr.update(
choices=[(t("tagger_format_prompt", lc), "prompt"), (t("tagger_format_raw", lc), "raw")],
label=t("tagger_format_label", lc), info=t("tagger_format_desc", lc),
),
gr.update(label=t("tagger_chips_label", lc)),
gr.update(label=t("tagger_chars_label", lc)),
gr.update(label=t("tagger_tags_label", lc), info=t("tagger_tags_desc", lc), placeholder=t("tagger_tags_ph", lc)),
gr.update(value="๐ " + t("tagger_copy", lc)),
gr.update(value=_tree_header("๐งน", "advanced_label", lc, "advanced_desc")),
gr.update(
choices=[(t("output_format_prompt", lc), "prompt"), (t("output_format_booru", lc), "booru")],
label=t("output_format_label", lc), info=t("output_format_desc", lc),
),
gr.update(label="โพ " + t("cleanup_label", lc)),
gr.update(label=t("blacklist_label", lc), info=t("blacklist_desc", lc), placeholder=t("blacklist_ph", lc)),
gr.update(label=t("blacklist_mirror", lc)),
gr.update(label=t("strip_quality", lc)),
gr.update(label=t("strip_artist", lc)),
gr.update(label=t("strip_lora", lc)),
gr.update(label=t("strip_meta", lc)),
gr.update(label=t("min_tags_label", lc), info=t("min_tags_desc", lc)),
gr.update(label="โพ " + t("user_presets_label", lc)),
gr.update(label=t("user_preset_name", lc), placeholder=t("user_preset_name_ph", lc)),
gr.update(label=t("user_preset_tags", lc), placeholder=t("user_preset_tags_ph", lc)),
gr.update(value=t("user_preset_save", lc)),
gr.update(value=t("user_preset_delete", lc)),
gr.update(value=f'โธ {t("user_preset_select", lc)}'),
gr.update(choices=_user_preset_choices(lc)),
gr.update(value=t("user_preset_apply", lc)),
gr.update(label=t("theme_label", lc), choices=[(t("theme_dark", lc), "dark"), (t("theme_light", lc), "light")]),
]
for cat in ALL_CATEGORIES:
cn = t(cat, lc)
icon = CATEGORY_ICONS.get(cat, "๐")
cat_desc_key = f"{cat}_desc"
updates.append(gr.update(label=f"{icon} {cn}", info=t(cat_desc_key, lc)))
for group in PRESET_GROUPS:
choices = [
(f"{PRESETS[p][f"icon"]} {PRESETS[p][f"label_{lc}"]}", p)
for p in group["presets"]
]
updates.append(gr.update(choices=choices))
for _ in range(MAX_OUTPUTS):
updates.append(gr.update(placeholder=t("result_placeholder", lc)))
for _ in range(MAX_OUTPUTS):
updates.append(gr.update(placeholder=t("neg_placeholder", lc)))
updates.append(gr.update(value=f'{t("categories_desc", lc)}
'))
updates.append(gr.update(value=f'{t("presets_desc", lc)}
'))
updates.append(gr.update(value=f'{t("results_desc", lc)}
'))
updates.append(gr.update(value=f'{t("hotkeys_hint", lc)}
'))
updates.append(gr.update(value=_format_artist_info([], lc)))
updates.append(gr.update(value=f'{t("analyzer_desc", lc)}
'))
updates.append(gr.update(value=_format_history_html(lc)))
updates.append(choice)
return updates
lang_outputs = [
title_html,
categories_header_html,
analyzer_header_html,
artist_header_html,
tandem_header_html,
presets_header_html,
results_header_html,
history_header_html,
] + [
prompt_input, model_trigger, model_dropdown, rating_trigger, rating_dropdown,
num_variations, creativity, weight_mode, mode_toggle, web_enrich_toggle, fx_toggle, generate_btn,
web_search_panel, web_search_input, web_search_btn,
artist_style_trigger, artist_style_dropdown, artist_search, artist_multiselect_trigger, artist_multiselect,
use_tandems, tandem_trigger, tandem_dropdown, seed_input, reload_btn,
] + [
tagger_header_html,
tagger_image,
tagger_gen_threshold,
tagger_char_threshold,
tagger_btn,
tagger_apply_btn,
tagger_output,
tagger_format,
tagger_chips,
tagger_chars,
tagger_tags_box,
tagger_copy_btn,
] + [
adv_header_html, output_format_radio, cleanup_acc,
blacklist_box, mirror_blacklist, strip_quality, strip_artist, strip_lora, strip_meta, min_tags,
presets_acc, user_preset_name, user_preset_tags, user_preset_save, user_preset_delete,
user_preset_trigger, user_preset_dropdown, user_preset_apply, theme_toggle,
] + category_checks + group_components + result_boxes + neg_boxes + [categories_desc_md, presets_desc_md, results_desc_md, hotkeys_hint_md, artist_info_md, analyzer_md, history_md,
lang_state]
lang_btn.change(
fn=update_lang,
inputs=[lang_btn],
outputs=lang_outputs,
)
theme_toggle.change(
fn=None,
inputs=[theme_toggle],
outputs=[],
js="(v) => { document.body.classList.toggle('whyx-light', v === 'light'); }",
)
if __name__ == "__main__":
_launch_kw = {}
if _GRADIO_MAJOR >= 6:
_launch_kw["theme"] = WhyxTheme()
_launch_kw["css"] = WHYX_CSS
_launch_kw["head"] = f""
# Gradio 5 SSR on HF Spaces breaks custom JS listeners under `elem_id=` โ
# disable SSR explicitly while keeping the theme/css/head in launch args.
_launch_kw.setdefault("ssr_mode", False)
demo.launch(**_launch_kw)