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"""
    {title}
    {empty}
    """ 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"""
    {title}
    {empty}
    """) 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"""
    {title}
    {empty}
    """) 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'
    {t(desc_key, lang)}
    ' return f"""
    {icon}
    {t(title_key, lang)} {desc}
    """ 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)