Spaces:
Running
Running
| 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 = '<span style="color:#34D399;font-weight:700;font-size:16px;">✓</span>'; | |
| 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: | |
| def _card_updates(positives): | |
| return tuple( | |
| gr.update(visible=bool((positives[i] or "").strip())) | |
| for i in range(MAX_OUTPUTS) | |
| ) | |
| try: | |
| if not prompt or not prompt.strip(): | |
| return tuple([""] * (MAX_OUTPUTS * 2)) + _card_updates([""] * MAX_OUTPUTS) | |
| parsed = parse_prompt(prompt) | |
| if parsed is None: | |
| msg = t("error_parse", lang) | |
| outs = [msg] + [""] * (MAX_OUTPUTS - 1) | |
| return tuple(outs) + tuple([""] * MAX_OUTPUTS) + _card_updates(outs) | |
| 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]) + _card_updates(out[:MAX_OUTPUTS]) | |
| except Exception as exc: | |
| import traceback | |
| traceback.print_exc() | |
| err_msg = t("generation_error", lang).format(exc=html.escape(str(exc))) | |
| outs = [err_msg] + [""] * (MAX_OUTPUTS - 1) | |
| return tuple(outs) + tuple([""] * MAX_OUTPUTS) + _card_updates(outs) | |
| 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'<div class="whyx-info-text">{t("input_placeholder", "en" if lang != "RU" else "ru")}</div>' | |
| parsed = parse_prompt(prompt) | |
| if parsed is None: | |
| return f'<div class="whyx-info-text">{t("error_parse", lang)}</div>' | |
| 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'<div class="whyx-info-text">{t("suggest_empty", lc)}</div>' | |
| lines = "".join( | |
| f'<li class="whyx-suggest-item">{html.escape(s)}</li>' for s in suggestions | |
| ) | |
| return ( | |
| f'<div class="whyx-suggest-box">' | |
| f'<div class="whyx-suggest-title">{t("suggest_header", lc)}</div>' | |
| f'<ul>{lines}</ul></div>' | |
| ) | |
| except Exception as exc: | |
| import traceback | |
| traceback.print_exc() | |
| return f'<div class="whyx-info-text">Error: {html.escape(str(exc))}</div>' | |
| def _preset_status(key: str, lang: str, **kwargs) -> str: | |
| return f'<div class="whyx-info-text">{t(key, lang).format(**kwargs) if kwargs else t(key, lang)}</div>' | |
| 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'<div class="whyx-info-text" style="color:var(--text-faint);font-size:12px;">{t("web_search_empty", lc)}</div>' | |
| try: | |
| results = search_tags(query, warehouse) | |
| parts = [] | |
| if results["local"]: | |
| parts.append(f'<div style="margin-top:6px;font-size:13px;font-weight:600;color:var(--text);">🔍 {t("web_search_offline", lc)} ({len(results["local"])})</div>') | |
| for r in results["local"][:15]: | |
| safe_tag = html.escape(r["tag"]) | |
| safe_cat = html.escape(r["category"]) | |
| parts.append(f'<span style="display:inline-block;background:var(--accent-glow);border:1px solid var(--accent-border);border-radius:4px;padding:1px 6px;margin:2px;font-size:12px;color:var(--text);">{safe_tag} <span style="color:var(--text-faint);font-size:10px;">[{safe_cat}]</span></span>') | |
| if results["web"]: | |
| parts.append(f'<div style="margin-top:8px;font-size:13px;font-weight:600;color:var(--text);">🌐 {t("web_search_online", lc)} ({len(results["web"])})</div>') | |
| for r in results["web"][:MAX_OUTPUTS]: | |
| safe_tag = html.escape(r.get("tag", "")) | |
| count = r.get("post_count", 0) | |
| parts.append(f'<span style="display:inline-block;background:var(--accent-glow);border:1px solid var(--accent-border);border-radius:4px;padding:1px 6px;margin:2px;font-size:12px;color:var(--text);">{safe_tag} <span style="color:var(--text-faint);font-size:10px;">({count} posts)</span></span>') | |
| if not parts: | |
| return f'<div class="whyx-info-text" style="font-size:12px;">{t("web_enrich_no_results", lc)}</div>' | |
| return '<div style="line-height:1.8;">' + "".join(parts) + '</div>' | |
| except Exception as exc: | |
| return f'<div style="color:#ef4444;font-size:12px;">{t("web_search_error", lc).format(exc=html.escape(str(exc)))}</div>' | |
| def _format_history_html(lang: str) -> str: | |
| lc = "ru" if lang == "RU" else "en" | |
| history = get_history() | |
| lines = [] | |
| lines.append("<div style='background:var(--input-bg);border:1px solid rgba(124,58,237,0.12);border-radius:10px;padding:10px 14px;margin-top:4px;'>") | |
| entries = history.get_all()[:5] | |
| if not entries: | |
| lines.append(f"<div style='color:var(--text-dim);font-size:12px;'>{t('history_empty', lc)}</div>") | |
| else: | |
| for entry in entries: | |
| liked = entry.get_liked_results() | |
| lines.append("<div style='border-bottom:1px solid rgba(79,70,229,0.06);padding:6px 0;'>") | |
| lines.append("<div style='font-size:11px;color:var(--text-dim);'>") | |
| lines.append(f"<span style='color:var(--text);'>{t('history_prompt', lc)}:</span> <span style='color:var(--text-faint);'>") | |
| p = entry.prompt[:80] + "..." if len(entry.prompt) > 80 else entry.prompt | |
| lines.append(f"{html.escape(p)}</span></div>") | |
| if liked: | |
| lines.append(f"<div style='margin-top:3px;font-size:11px;color:#F43F5E;'>♥ {len(liked)} {t('history_liked', lc).lower()}</div>") | |
| lines.append("</div>") | |
| favs = history.get_favorites() | |
| if favs: | |
| lines.append("<div style='border-top:1px solid rgba(79,70,229,0.08);margin-top:8px;padding-top:8px;'>") | |
| lines.append(f"<div style='font-size:12px;color:var(--text);font-weight:600;margin-bottom:4px;'>♥ {t('favorites_label', lc)}</div>") | |
| for entry, idx, text in favs[:5]: | |
| t_text = text[:60] + "..." if len(text) > 60 else text | |
| lines.append(f"<div style='font-size:11px;color:var(--text-dim);padding:2px 0;'><span style='color:#F43F5E;'>♥</span> {html.escape(t_text)}</div>") | |
| lines.append("</div>") | |
| else: | |
| lines.append("<div style='border-top:1px solid rgba(79,70,229,0.08);margin-top:8px;padding-top:8px;'>") | |
| lines.append(f"<div style='font-size:11px;color:var(--text-faint);'>{t('history_no_likes', lc)}</div>") | |
| lines.append("</div>") | |
| lines.append("</div>") | |
| 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""" | |
| <div class="whyx-artist-preview"> | |
| <div class="whyx-artist-preview-title">{title}</div> | |
| <div class="whyx-artist-preview-empty">{empty}</div> | |
| </div> | |
| """ | |
| 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"<a class='whyx-artist-link' href='{safe_url}' target='_blank'>{t('danbooru_link', lang)}</a>" if url else "" | |
| lines.append(f""" | |
| <div style='margin:8px 0;padding:8px 0;border-bottom:1px solid rgba(79,70,229,0.08);'> | |
| <div style='display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap;'> | |
| <strong style='color:var(--text);font-size:14px;'>{safe_name}</strong> | |
| {link} | |
| </div> | |
| <div style='color:var(--text-dim);font-size:12px;margin-top:4px;line-height:1.4;'>{safe_desc}</div> | |
| </div> | |
| """) | |
| # 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"<div style='margin-top:8px;font-size:12px;color:#7C3AED;'>✨ {label}: {names}</div>") | |
| return "<div class='whyx-artist-preview'><div class='whyx-artist-preview-title'>" + title + "</div>" + "".join(lines) + "</div>" | |
| 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""" | |
| <div class="whyx-artist-preview"> | |
| <div class="whyx-artist-preview-title">{title}</div> | |
| <div class="whyx-artist-preview-empty">{empty}</div> | |
| </div> | |
| """) | |
| 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""" | |
| <div class="whyx-artist-preview"> | |
| <div class="whyx-artist-preview-title">{title}</div> | |
| <div class="whyx-artist-preview-empty">{empty}</div> | |
| </div> | |
| """) | |
| names = ", ".join(html.escape(a["tag"]) for a in artists) | |
| label = t("top_artists_in_style", lang) | |
| return gr.update(value=f"<div class='whyx-artist-preview'><div class='whyx-artist-preview-title'>{title}</div><div style='font-size:12px;color:#7C3AED;'>✨ {label}: {names}</div></div>") | |
| def _build_tagger_html(result: dict, lc: str) -> str: | |
| lines = [] | |
| lines.append("<div class='whyx-tagger-panel'>") | |
| # ---- Natural-language caption (Florence) -------------------------------- | |
| nl_caption = (result.get("nl_caption") or "").strip() | |
| if nl_caption: | |
| lines.append( | |
| f"<div class='whyx-tagger-panel-title'>{t('tagger_nl_caption', lc)}</div>" | |
| f"<div class='whyx-tagger-nl-caption'>{html.escape(nl_caption)}</div>" | |
| ) | |
| # ---- 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"<div class='whyx-tagger-panel-title' style='margin-top:10px;'>🧍 {t('tagger_pose_label', lc)}</div>") | |
| if people: | |
| lines.append(f"<div class='whyx-tagger-pose-people'>{html.escape(pose_label)}</div>") | |
| if pose_tags: | |
| lines.append( | |
| "<div class='whyx-tagger-pose-tags'>" | |
| + ", ".join(html.escape(p) for p in pose_tags) | |
| + "</div>" | |
| ) | |
| # ---- 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"<div class='whyx-tagger-panel-title' style='margin-top:10px;'>{emoji} {t(label_key, lc)}</div>" | |
| ) | |
| lines.append( | |
| f"<div class='{css_class}'>" | |
| + ", ".join(html.escape(p) for p in smart_tags) | |
| + "</div>" | |
| ) | |
| # ---- Categorized tags (emotion / lighting / style / concept / palette) ---- | |
| for key, label_key, emoji, css_class in [ | |
| ("emotion_tags", "tagger_emotion_label", "🎭", "whyx-tagger-emotion-tags"), | |
| ("lighting_tags", "tagger_lighting_label", "💡", "whyx-tagger-lighting-tags"), | |
| ("style_tags", "tagger_style_label", "🎨", "whyx-tagger-style-tags"), | |
| ("concept_tags", "tagger_concept_label", "💭", "whyx-tagger-concept-tags"), | |
| ("palette_tags", "tagger_palette_label", "🌈", "whyx-tagger-palette-tags"), | |
| ]: | |
| cat_tags = result.get(key) or [] | |
| if cat_tags: | |
| lines.append( | |
| f"<div class='whyx-tagger-panel-title' style='margin-top:10px;'>{emoji} {t(label_key, lc)}</div>" | |
| ) | |
| lines.append( | |
| f"<div class='{css_class}'>" | |
| + ", ".join(html.escape(p) for p in cat_tags) | |
| + "</div>" | |
| ) | |
| # ---- Rating distribution --------------------------------------------------- | |
| lines.append(f"<div class='whyx-tagger-panel-title' style='margin-top:10px;'>{t('tagger_ratings', lc)}</div>") | |
| 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) | |
| lines.append( | |
| f"<div class='whyx-tagger-rating-row'>" | |
| f"<span class='whyx-tagger-rating-name' style='color:{color};'>{name}</span>" | |
| f"<span class='whyx-tagger-rating-track'><span class='whyx-tagger-rating-fill' style='width:{pct}%;background:{color};'></span></span>" | |
| f"<span class='whyx-tagger-rating-pct'>({score:.1%})</span></div>" | |
| ) | |
| if result["characters"]: | |
| lines.append(f"<div class='whyx-tagger-panel-title' style='margin-top:10px;'>{t('tagger_characters', lc)}</div>") | |
| lines.append("<div class='whyx-tagger-badges'>") | |
| for name, score in list(result["characters"].items())[:6]: | |
| safe = html.escape(name.replace("_", " ").replace("(", "\\(").replace(")", "\\)")) | |
| lines.append(f"<span class='whyx-tagger-char-badge'>{safe} <span class='whyx-tagger-badge-pct'>{score:.0%}</span></span>") | |
| lines.append("</div>") | |
| lines.append(f"<div class='whyx-tagger-panel-title' style='margin-top:10px;'>{t('tagger_top_general', lc)}</div>") | |
| lines.append("<div class='whyx-tagger-gen-list'>") | |
| for name, score in list(result["general"].items())[:14]: | |
| safe = html.escape(name.replace("_", " ").replace("(", "\\(").replace(")", "\\)")) | |
| pct = int(score * 100) | |
| lines.append( | |
| f"<div class='whyx-tagger-gen-row'>" | |
| f"<span class='whyx-tagger-gen-track'><span class='whyx-tagger-gen-fill' style='width:{pct}%;'></span></span>" | |
| f"<span class='whyx-tagger-gen-name'>{safe}</span>" | |
| f"<span class='whyx-tagger-badge-pct'>{score:.0%}</span></div>" | |
| ) | |
| lines.append("</div>") | |
| lines.append("</div>") | |
| 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|<mode>" 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'<div style="color:#F87171;font-size:12px;">{t("tagger_no_image", lc)}</div>'), | |
| 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'<div style="color:#FBBF24;font-size:13px;line-height:1.5;">{t("tagger_unavailable", lc)}</div>' | |
| 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'<div style="color:#FBBF24;font-size:12px;">{t("tagger_no_tags", lc)}</div>' | |
| 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'<div style="color:#F87171;font-size:12px;">{err}</div>'), | |
| 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'<div style="color:#F87171;font-size:12px;">{t("tagger_no_image", lc)}</div>'), | |
| 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'<div style="color:#FBBF24;font-size:13px;line-height:1.5;">{t("tagger_unavailable", lc)}</div>' | |
| 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'<div style="color:#FBBF24;font-size:12px;">{t("tagger_caption_empty", lc)}</div>' | |
| 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'<div style="color:#F87171;font-size:12px;">{err}</div>'), | |
| 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; | |
| } | |
| /* --- Light theme component overrides --- */ | |
| body.whyx-light { | |
| --tree-node-bg: rgba(255,255,255,0.92); | |
| --tree-node-border: rgba(15,23,42,0.08); | |
| --tree-node-border-hover: rgba(15,23,42,0.16); | |
| --tree-branch-hover: rgba(79,70,229,0.06); | |
| --tree-leaf-hover: rgba(241,245,249,0.9); | |
| } | |
| body.whyx-light::before { opacity: 0.45 !important; } | |
| body.whyx-light [role="listbox"], body.whyx-light ul.options, body.whyx-light ul.dropdown-options, body.whyx-light [data-testid="dropdown-options"] { | |
| background: rgba(255,255,255,0.98) !important; | |
| border: 1px solid rgba(15,23,42,0.12) !important; | |
| box-shadow: 0 8px 32px rgba(15,23,42,0.16), 0 0 0 1px rgba(79,70,229,0.05) !important; | |
| } | |
| body.whyx-light .gradio-dropdown { background: #FFFFFF !important; } | |
| body.whyx-light .whyx-tagger-char-badge { background: linear-gradient(135deg, rgba(129,140,248,0.14), rgba(99,102,241,0.1)) !important; border-color: rgba(99,102,241,0.35) !important; color: #3730A3 !important; } | |
| body.whyx-light .whyx-tagger-badge-pct { color: #6366F1 !important; } | |
| body.whyx-light .progress-level { background: rgba(255,255,255,0.85) !important; border-color: rgba(124,58,237,0.18) !important; } | |
| body.whyx-light .whyx-tagger-chips label:has(input:checked) { color: #0F766E !important; } | |
| body.whyx-light .whyx-slide-trigger { background: linear-gradient(135deg, rgba(255,255,255,0.85), rgba(241,245,249,0.65)) !important; border-color: rgba(15,23,42,0.12) !important; } | |
| body.whyx-light *::-webkit-scrollbar-track { background: rgba(15,23,42,0.05) !important; } | |
| body.whyx-light *::-webkit-scrollbar-thumb { background: rgba(15,23,42,0.18) !important; } | |
| body.whyx-light *::-webkit-scrollbar-thumb:hover { background: rgba(15,23,42,0.3) !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-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-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-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-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: 4px !important; display: flex !important; align-items: center !important; gap: 8px !important; } | |
| .whyx-tagger-rating-name { width: 92px !important; display: inline-block !important; font-weight: 600 !important; flex-shrink: 0 !important; } | |
| .whyx-tagger-rating-track { flex: 1 !important; height: 6px !important; background: var(--chip-bg) !important; border: 1px solid var(--chip-border) !important; border-radius: var(--radius-pill) !important; overflow: hidden !important; } | |
| .whyx-tagger-rating-fill { display: block !important; height: 100% !important; border-radius: var(--radius-pill) !important; transition: width 0.4s ease !important; } | |
| .whyx-tagger-rating-pct { color: var(--text-faint) !important; flex-shrink: 0 !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-track { width: 90px !important; height: 5px !important; background: var(--chip-bg) !important; border: 1px solid var(--chip-border) !important; border-radius: var(--radius-pill) !important; overflow: hidden !important; flex-shrink: 0 !important; } | |
| .whyx-tagger-gen-fill { display: block !important; height: 100% !important; background: linear-gradient(90deg, #5AC8F5, #38BDF8) !important; border-radius: var(--radius-pill) !important; transition: width 0.4s ease !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; } | |
| .whyx-tagger-seg-tags { font-size: 12px !important; color: var(--text-dim) !important; line-height: 1.4 !important; background: var(--input-bg) !important; border: 1px solid rgba(16,185,129,0.16) !important; border-radius: var(--radius-sm) !important; padding: 6px 10px !important; margin-top: 4px !important; } | |
| .whyx-tagger-depth-tags { font-size: 12px !important; color: var(--text-dim) !important; line-height: 1.4 !important; background: var(--input-bg) !important; border: 1px solid rgba(37,99,235,0.18) !important; border-radius: var(--radius-sm) !important; padding: 6px 10px !important; margin-top: 4px !important; } | |
| .whyx-tagger-emotion-tags { font-size: 12px !important; color: var(--text-dim) !important; line-height: 1.4 !important; background: var(--input-bg) !important; border: 1px solid rgba(244,63,94,0.18) !important; border-radius: var(--radius-sm) !important; padding: 6px 10px !important; margin-top: 4px !important; } | |
| .whyx-tagger-lighting-tags { font-size: 12px !important; color: var(--text-dim) !important; line-height: 1.4 !important; background: var(--input-bg) !important; border: 1px solid rgba(251,191,36,0.18) !important; border-radius: var(--radius-sm) !important; padding: 6px 10px !important; margin-top: 4px !important; } | |
| .whyx-tagger-style-tags { font-size: 12px !important; color: var(--text-dim) !important; line-height: 1.4 !important; background: var(--input-bg) !important; border: 1px solid rgba(168,85,247,0.18) !important; border-radius: var(--radius-sm) !important; padding: 6px 10px !important; margin-top: 4px !important; } | |
| .whyx-tagger-concept-tags { font-size: 12px !important; color: var(--text-dim) !important; line-height: 1.4 !important; background: var(--input-bg) !important; border: 1px solid rgba(59,130,246,0.18) !important; border-radius: var(--radius-sm) !important; padding: 6px 10px !important; margin-top: 4px !important; } | |
| .whyx-tagger-palette-tags { font-size: 12px !important; color: var(--text-dim) !important; line-height: 1.4 !important; background: var(--input-bg) !important; border: 1px solid rgba(16,185,129,0.18) !important; border-radius: var(--radius-sm) !important; padding: 6px 10px !important; margin-top: 4px !important; } | |
| .whyx-tagger-control-img { border-radius: var(--radius-md) !important; overflow: hidden !important; border: 1px solid var(--glass-border) !important; } | |
| .whyx-tagger-control-img img { border-radius: var(--radius-md) !important; } | |
| .whyx-suggest-box { background: var(--input-bg) !important; border: 1px solid rgba(124,58,237,0.14) !important; border-radius: var(--radius-md) !important; padding: 10px 14px !important; margin-top: 6px !important; animation: whyxFadeIn 0.3s ease both !important; } | |
| .whyx-suggest-title { font-size: 12px !important; font-weight: 600 !important; color: var(--text) !important; margin-bottom: 6px !important; } | |
| .whyx-suggest-box ul { margin: 0 !important; padding-left: 18px !important; } | |
| .whyx-suggest-item { font-size: 12px !important; color: var(--text-dim) !important; line-height: 1.5 !important; padding: 1px 0 !important; } | |
| .whyx-chip-group { display: flex !important; flex-wrap: wrap !important; gap: 6px !important; } | |
| .whyx-chip-group 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 12px !important; font-size: 11px !important; cursor: pointer !important; transition: all var(--transition-fast) !important; user-select: none !important; } | |
| .whyx-chip-group label:hover { background: var(--accent-glow) !important; border-color: var(--accent-border) !important; } | |
| .whyx-chip-group label:has(input:checked) { background: var(--accent-glow) !important; border-color: var(--accent-border) !important; color: var(--text-accent) !important; } | |
| .whyx-checkbox label { font-size: 12px !important; color: var(--text-dim) !important; } | |
| .whyx-strip-row { gap: 10px !important; flex-wrap: wrap !important; } | |
| .whyx-strip-row .gradio-checkbox { min-width: 0 !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; } | |
| .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; } | |
| @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; } | |
| /* Result cards: compact wrapped layout instead of a tall vertical stack */ | |
| #root .whyx-result-card { flex-direction: row !important; flex-wrap: wrap !important; align-items: flex-start !important; gap: 6px !important; } | |
| #root .whyx-result-card > .whyx-rc-num { width: auto !important; flex: 0 0 auto !important; } | |
| #root .whyx-result-card > .whyx-rc-pos { width: auto !important; flex: 1 1 60% !important; min-width: 0 !important; } | |
| #root .whyx-result-card > .whyx-rc-copypos, | |
| #root .whyx-result-card > .whyx-rc-heart { width: auto !important; flex: 0 0 auto !important; } | |
| #root .whyx-result-card > .whyx-rc-neg { width: auto !important; flex: 1 1 70% !important; min-width: 0 !important; } | |
| #root .whyx-result-card > .whyx-rc-copyneg { width: auto !important; flex: 0 0 auto !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); } | |
| } | |
| /* 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'<div class="whyx-tree-header-desc">{t(desc_key, lang)}</div>' | |
| return f""" | |
| <div class="whyx-tree-header"> | |
| <span class="whyx-tree-header-icon">{icon}</span> | |
| <div> | |
| <span class="whyx-tree-header-title">{t(title_key, lang)}</span> | |
| {desc} | |
| </div> | |
| </div> | |
| """ | |
| 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"<script>{HOTKEYS_JS}</script>" | |
| 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""" | |
| <div> | |
| <div class="whyx-title">Whyx-PROmpTea</div> | |
| <div class="whyx-subtitle">{t("app_subtitle", "en")}</div> | |
| </div> | |
| """) | |
| 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('<hr class="whyx-divider">') | |
| current_preset_state = gr.State("") | |
| with gr.Tabs(): | |
| # ===== TAB 1: Generate ===== | |
| tab_generate = gr.TabItem(f"✨ {t('tab_generate', 'en')}") | |
| with tab_generate: | |
| 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'<div class="whyx-info-text">{t("categories_desc", "en")}</div>', | |
| 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'<div class="whyx-info-text">{t("presets_desc", "en")}</div>', | |
| visible=True, | |
| ) | |
| preset_groups = {} | |
| for group in PRESET_GROUPS: | |
| gr.Markdown( | |
| f'<div class="whyx-preset-group-label">{group["icon"]} {group["label_en"]}</div>' | |
| ) | |
| 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'<div style="text-align:center;font-size:11px;color:var(--text-dim);margin-top:6px;">{t("hotkeys_hint", "en")}</div>', | |
| 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'<div class="whyx-info-text">{t("results_desc", "en")}</div>', | |
| ) | |
| result_boxes = [] | |
| neg_boxes = [] | |
| heart_btns = [] | |
| result_cards = [] | |
| tooltip_spans = [] # (component, "copy" | "fav") for i18n updates | |
| for i in range(MAX_OUTPUTS): | |
| with gr.Row(elem_classes=["whyx-result-card"], visible=False) as card: | |
| result_cards.append(card) | |
| with gr.Column(scale=0, min_width=40, elem_classes=["whyx-rc-num"]): | |
| gr.HTML(f'<div class="whyx-result-num">#{i+1}</div>') | |
| with gr.Column(scale=1, elem_classes=["whyx-rc-pos"]): | |
| 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, elem_classes=["whyx-rc-copypos"]): | |
| cb_pos = gr.Button( | |
| "📋", | |
| size="sm", | |
| variant="secondary", | |
| scale=0, | |
| min_width=32, | |
| elem_classes=["whyx-copy-btn", "whyx-tooltip"], | |
| elem_id=f"copy-btn-pos-{i}", | |
| ) | |
| cb_pos.click(fn=None, inputs=[rb], outputs=[], js=COPY_JS) | |
| tt_copy_pos = gr.HTML(f'<span class="whyx-tooltip-text">{t("tooltip_copy", "en")}</span>') | |
| tooltip_spans.append((tt_copy_pos, "copy")) | |
| with gr.Column(scale=0, min_width=46, elem_classes=["whyx-rc-heart"]): | |
| hb = gr.Button( | |
| "♡", | |
| size="sm", | |
| variant="secondary", | |
| scale=0, | |
| min_width=36, | |
| elem_classes=["whyx-heart-btn", "whyx-tooltip"], | |
| elem_id=f"heart-btn-{i}", | |
| ) | |
| heart_btns.append(hb) | |
| tt_fav = gr.HTML(f'<span class="whyx-tooltip-text">{t("tooltip_favorite", "en")}</span>') | |
| tooltip_spans.append((tt_fav, "fav")) | |
| with gr.Column(scale=1, elem_classes=["whyx-rc-neg"]): | |
| 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, elem_classes=["whyx-rc-copyneg"]): | |
| cb_neg = gr.Button( | |
| "📋", | |
| size="sm", | |
| variant="secondary", | |
| scale=0, | |
| min_width=32, | |
| elem_classes=["whyx-copy-btn", "whyx-tooltip"], | |
| elem_id=f"copy-btn-neg-{i}", | |
| ) | |
| cb_neg.click(fn=None, inputs=[nb], outputs=[], js=COPY_JS) | |
| tt_copy_neg = gr.HTML(f'<span class="whyx-tooltip-text">{t("tooltip_copy", "en")}</span>') | |
| tooltip_spans.append((tt_copy_neg, "copy")) | |
| result_boxes.append(rb) | |
| neg_boxes.append(nb) | |
| # ===== TAB 2: Tagger ===== | |
| tab_tagger = gr.TabItem(f"🖼️ {t('tab_tagger', 'en')}") | |
| with tab_tagger: | |
| 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'<div class="whyx-info-text" style="font-size:12px;">{t("tagger_desc", "en")}</div>', | |
| ) | |
| 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 ===== | |
| tab_tools = gr.TabItem(f"🔧 {t('tab_tools', 'en')}") | |
| with tab_tools: | |
| 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'<div class="whyx-info-text">{t("analyzer_desc", "en")}</div>', | |
| 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'<div class="whyx-info-text" style="color:rgba(148,163,184,0.6);font-size:12px;">{t("web_search_empty", "en")}</div>', | |
| ) | |
| # ===== TAB 4: History ===== | |
| tab_history = gr.TabItem(f"📜 {t('tab_history', 'en')}") | |
| with tab_history: | |
| 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'<div class="whyx-info-text" style="color:var(--text-faint);font-size:12px;">{t("web_search_empty", "ru" if lang == "RU" else "en")}</div>', | |
| 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 + result_cards, | |
| ).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""" | |
| <div> | |
| <div class="whyx-title">Whyx-PROmpTea</div> | |
| <div class="whyx-subtitle">{t("app_subtitle", lc)}</div> | |
| </div> | |
| """), | |
| 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'<div class="whyx-info-text" style="font-size:12px;">{t("tagger_desc", lc)}</div>'), | |
| 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'<div class="whyx-info-text">{t("categories_desc", lc)}</div>')) | |
| updates.append(gr.update(value=f'<div class="whyx-info-text">{t("presets_desc", lc)}</div>')) | |
| updates.append(gr.update(value=f'<div class="whyx-info-text">{t("results_desc", lc)}</div>')) | |
| updates.append(gr.update(value=f'<div style="text-align:center;font-size:11px;color:var(--text-dim);margin-top:6px;">{t("hotkeys_hint", lc)}</div>')) | |
| updates.append(gr.update(value=_format_artist_info([], lc))) | |
| updates.append(gr.update(value=f'<div class="whyx-info-text">{t("analyzer_desc", lc)}</div>')) | |
| updates.append(gr.update(value=_format_history_html(lc))) | |
| updates.append(gr.update(label=f"✨ {t('tab_generate', lc)}")) | |
| updates.append(gr.update(label=f"🖼️ {t('tab_tagger', lc)}")) | |
| updates.append(gr.update(label=f"🔧 {t('tab_tools', lc)}")) | |
| updates.append(gr.update(label=f"📜 {t('tab_history', lc)}")) | |
| for _comp, kind in tooltip_spans: | |
| key = "tooltip_copy" if kind == "copy" else "tooltip_favorite" | |
| updates.append(gr.update(value=f'<span class="whyx-tooltip-text">{t(key, lc)}</span>')) | |
| 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, | |
| tab_generate, tab_tagger, tab_tools, tab_history] + [c for c, _k in tooltip_spans] + [lang_state] | |
| lang_btn.change( | |
| fn=update_lang, | |
| inputs=[lang_btn], | |
| outputs=lang_outputs, | |
| ) | |
| lang_btn.change( | |
| fn=None, | |
| inputs=[lang_btn], | |
| outputs=[], | |
| js="(v) => { try { localStorage.setItem('whyx-lang', v); } catch(e) {} }", | |
| ) | |
| theme_toggle.change( | |
| fn=None, | |
| inputs=[theme_toggle], | |
| outputs=[], | |
| js="(v) => { document.body.classList.toggle('whyx-light', v === 'light'); try { localStorage.setItem('whyx-theme', v); } catch(e) {} }", | |
| ) | |
| # Restore persisted theme + language on page load: the JS reads | |
| # localStorage, applies the body class / radio value, then the standard | |
| # update_lang chain re-renders every localized label. | |
| demo.load( | |
| fn=None, | |
| inputs=[], | |
| outputs=[theme_toggle], | |
| js="() => { let t = 'dark'; try { t = localStorage.getItem('whyx-theme') || 'dark'; } catch(e) {} document.body.classList.toggle('whyx-light', t === 'light'); return t; }", | |
| ).then( | |
| fn=None, | |
| inputs=[], | |
| outputs=[lang_btn], | |
| js="() => { try { return localStorage.getItem('whyx-lang') || 'EN'; } catch(e) { return 'EN'; } }", | |
| ).then( | |
| fn=update_lang, | |
| inputs=[lang_btn], | |
| outputs=lang_outputs, | |
| ) | |
| if __name__ == "__main__": | |
| _launch_kw = {} | |
| if _GRADIO_MAJOR >= 6: | |
| _launch_kw["theme"] = WhyxTheme() | |
| _launch_kw["css"] = WHYX_CSS | |
| _launch_kw["head"] = f"<script>{HOTKEYS_JS}</script>" | |
| # 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) | |