"""Tools tab: prompt analyzer, artist recommender and web tag search.""" import html import time import gradio as gr from src.i18n import t from src.prompt_analyzer import analyze_prompt, format_analysis_html, format_score_badge from src.ui.common import ( MAX_OUTPUTS, warehouse, STYLECHOICES, ARTISTCHOICES, all_artists, build_artist_choice, tree_header, ) # Debounce for prompt analyzer (300ms) _ANALYZER_LAST = {"time": 0.0, "prompt": "", "html": "", "badge": ""} def web_search_empty_html(lc: str) -> str: """The empty-result strip. Kept in one place because it is written three times over: the initial value, the clear button's reset and update_lang. Duplicating it is how the strip ended up still English after a switch to RU/ZH -- two of the three copies were never re-rendered. The colour is the token, not rgba(): the hard-coded slate ignored every theme but the default one. """ return ( '
' + t("web_search_empty", lc) + "
" ) def on_web_search(query: str, lang: str) -> str: from src.tag_searcher import search_tags lc = lang.lower() if lang in ("EN", "RU", "ZH") else "en" if not query or not query.strip(): return f'
{t("web_search_empty", lc)}
' try: results = search_tags(query, warehouse) parts = [] if results["local"]: parts.append(f'
🔍 {t("web_search_offline", lc)} ({len(results["local"])})
') for r in results["local"][:15]: safe_tag = html.escape(r["tag"]) safe_cat = html.escape(r["category"]) parts.append(f'{safe_tag} [{safe_cat}]') if results["web"]: parts.append(f'
🌐 {t("web_search_online", lc)} ({len(results["web"])})
') for r in results["web"][:MAX_OUTPUTS]: safe_tag = html.escape(r.get("tag", "")) count = r.get("post_count", 0) parts.append(f'{safe_tag} ({count} posts)') if not parts: return f'
{t("web_enrich_no_results", lc)}
' return '
' + "".join(parts) + '
' except Exception as exc: return f'
{t("web_search_error", lc).format(exc=html.escape(str(exc)))}
' def on_artist_filter_change(style: str, query: str): try: from src.artist_recommender import recommend_artists filtered = recommend_artists(warehouse, query=query or "", style=style or "", limit=50) except Exception: 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", [])) ] if not q and len(filtered) > 50: filtered = sorted(filtered, key=lambda a: a.get("popularity", 0), reverse=True)[:50] choices = [(build_artist_choice(a), a["tag"]) for a in filtered] return gr.update(choices=choices, value=[]) def format_artist_info(artist_names: list[str], lang: str = "en") -> str: title = t("artist_preview_title", lang) if not artist_names: empty = t("artist_preview_empty", lang) return f"""
{title}
{empty}
""" lines = [] for name in artist_names: safe_name = html.escape(name) # Use vivid recommender description (local, no network) with fallback to warehouse try: from src.artist_recommender import get_vivid_description as _vivid desc = _vivid(warehouse, name) # If vivid is generic, fallback to warehouse description if "not in local pool" in desc: desc = warehouse.get_artist_description(name, lang) or desc except Exception: desc = warehouse.get_artist_description(name, lang) url = warehouse.get_artist_danbooru_url(name) if desc: safe_url = html.escape(url) if url else "" safe_desc = html.escape(desc) link = f"{t('danbooru_link', lang)}" if url else "" lines.append(f"""
{safe_name} {link}
{safe_desc}
""") # Similar artists from the first selected one (vivid recommender) if len(artist_names) == 1: try: from src.artist_recommender import get_similar_artists as _similar similar = _similar(warehouse, artist_names[0], limit=3) except Exception: similar = warehouse.get_similar_artists(artist_names[0], limit=3) if similar: names = ", ".join(html.escape(a["tag"]) for a in similar) label = t("similar_artists", lang) lines.append(f"
✨ {label}: {names}
") return "
" + title + "
" + "".join(lines) + "
" def on_artist_selection_change(selected_artists, lang): return gr.update(value=format_artist_info(selected_artists or [], lang)) def on_artist_style_recommendations(style: str, lang: str): title = t("artist_preview_title", lang) if not style: empty = t("artist_preview_empty", lang) return gr.update(value=f"""
{title}
{empty}
""") artists = warehouse.get_artists_by_style(style) artists = sorted(artists, key=lambda a: a.get("popularity", 0), reverse=True)[:3] if not artists: empty = t("artist_preview_empty", lang) return gr.update(value=f"""
{title}
{empty}
""") names = ", ".join(html.escape(a["tag"]) for a in artists) label = t("top_artists_in_style", lang) return gr.update(value=f"
{title}
✨ {label}: {names}
") def on_prompt_analyze(prompt: str, lang: str): # Debounce 300ms — avoid parsing on every keystroke now = time.time() key = (prompt or "").strip() if key == _ANALYZER_LAST["prompt"] and (now - _ANALYZER_LAST["time"]) < 0.30: return gr.update(value=_ANALYZER_LAST["html"]), gr.update(value=_ANALYZER_LAST["badge"]) lc = lang.lower() if lang in ("EN", "RU", "ZH") else "en" data = analyze_prompt(prompt, warehouse) html_out = format_analysis_html(data, lc) badge_out = format_score_badge(data, lc) _ANALYZER_LAST["time"] = now _ANALYZER_LAST["prompt"] = key _ANALYZER_LAST["html"] = html_out _ANALYZER_LAST["badge"] = badge_out return gr.update(value=html_out), gr.update(value=badge_out) def build(lang_state) -> dict: """Build the Tools tab inside the current Blocks context.""" c: dict = {} with gr.TabItem(f"🔧 {t('tab_tools', 'en')}") as tab_tools: with gr.Column(elem_classes=["art-tree"]): # Tree root: Analyzer with gr.Column(elem_classes=["art-tree-node"]): analyzer_header_html = gr.HTML(tree_header("📊", "analyzer_section", "en")) analyzer_md = gr.Markdown( value=f'
{t("analyzer_desc", "en")}
', visible=True, ) # Tree root: Artist (slide-down) with gr.Column(elem_classes=["art-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=["art-slide-trigger"], elem_id="art-trig-artist-style", ) artist_search_trigger = gr.Button( f'▸ {t("artist_search_label", "en")}', elem_classes=["art-slide-trigger"], elem_id="art-trig-artist-search", ) artist_multiselect_trigger = gr.Button( f'▸ {t("artist_select", "en")}', elem_classes=["art-slide-trigger"], elem_id="art-trig-artist-multiselect", ) artist_style_dropdown = gr.Radio( choices=STYLECHOICES, value="", label="", show_label=False, elem_id="art-panel-artist-style", elem_classes=["art-slide-panel", "art-slide-closed", "art-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="art-panel-artist-search", elem_classes=["art-slide-panel", "art-slide-closed"], ) artist_multiselect = gr.CheckboxGroup( choices=ARTISTCHOICES, value=[], label="", show_label=False, elem_id="art-panel-artist-multiselect", elem_classes=["art-slide-panel", "art-slide-closed", "art-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: Web Search with gr.Column(elem_classes=["art-tree-node"]): web_search_panel = gr.Accordion(t("web_search_label", "en"), open=False, elem_classes=["art-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=web_search_empty_html("en")) c.update( tab_tools=tab_tools, analyzer_header_html=analyzer_header_html, analyzer_md=analyzer_md, artist_header_html=artist_header_html, artist_style_trigger=artist_style_trigger, artist_style_dropdown=artist_style_dropdown, artist_search_trigger=artist_search_trigger, artist_search=artist_search, artist_multiselect_trigger=artist_multiselect_trigger, artist_multiselect=artist_multiselect, artist_info_md=artist_info_md, web_search_panel=web_search_panel, web_search_input=web_search_input, web_search_btn=web_search_btn, web_search_clear=web_search_clear, web_search_output=web_search_output, ) 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], ) web_search_clear.click( fn=lambda lang: web_search_empty_html(lang.lower() if lang in ("EN", "RU", "ZH") else "en"), inputs=[lang_state], outputs=[web_search_output], ) return c