import gradio as gr import requests import random import time import html import os # --- CONFIGURATION --- API_KEY = os.environ.get("GELBOORU_API_KEY") USER_ID = os.environ.get("GELBOORU_USER_ID") HEADERS = {"User-Agent": "GelbooruTagFetcher/10.0 (Python)"} ENDPOINT = "https://gelbooru.com/index.php" # --- DATA LISTS (RAW VALUES) --- FRANCHISES_RAW =[ "genshin_impact", "pokemon", "blue_archive", "arknights", "kancolle", "fire_emblem", "azur_lane", "honkai", "umamusume", "girls_und_panzer", "girls'_frontline", "zenless_zone_zero", "granblue_fantasy", "kemono_friends", "nikke", "project_moon", "wuthering_waves", "league_of_legends", "princess_connect!", "chainsaw_man", "guilty_gear", "reverse:1999", "mega_man", "dq", "tales" ] FRANCHISE_SUBSTITUTIONS = { "dq": "dragon quest", "tales": "tales of (series)" } FRANCHISE_EXCEPTIONS = { "fire_emblem": ["falchion_(fire_emblem)"], "arknights":["infection_monitor_(arknights)", "oripathy_lesion_(arknights)"], "project_moon":["e.g.o_(project_moon)", "(identity)"], "zenless_zone_zero":["bangboo_(zenless_zone_zero)"], "wuthering_waves":["pangu_terminal_(wuthering_waves)", "tacet_discord_(wuthering_waves)", "blake_bloom_(wuthering_waves)", "tacet_mark_(wuthering_waves)"], "league_of_legends":["spirit_blossom_(league_of_legends)", "k/da_(league_of_legends)", "star_guardian_(league_of_legends)"], "princess_connect!": ["gourmet_guild_(princess_connect!)"], "chainsaw_man":["i'll_teach_you_everything_(chainsaw_man)"], "dq":["dqn_(dqnww)", "dancer's_costume_(dq)"], "tales":["momdy_(talesshinja)", "normin_(tales)"], "genshin_impact":["vision_(genshin_impact)"] } UNWANTED = ["costume"] HAIR_COLORS_RAW =[ "Any", "white_hair", "red_hair", "purple_hair", "pink_hair", "orange_hair", "grey_hair", "green_hair", "brown_hair", "blue_hair", "blonde_hair", "black_hair", "aqua_hair" ] # --- UI FORMATTERS --- FRANCHISE_CHOICES =[ (FRANCHISE_SUBSTITUTIONS.get(f, f).replace("_", " ").title(), f) for f in FRANCHISES_RAW ] HAIR_CHOICES =[ (h.replace("_", " ").title(), h) for h in HAIR_COLORS_RAW ] # --- BACKEND FUNCTIONS --- def get_tag_count(tag_name): """Checks how many posts a specific tag has globally.""" params = { "page": "dapi", "s": "tag", "q": "index", "json": 1, "name": tag_name } if API_KEY and USER_ID: params["api_key"] = API_KEY params["user_id"] = USER_ID try: response = requests.get(ENDPOINT, params=params, headers=HEADERS) response.raise_for_status() data = response.json() tags = data.get("tag",[]) if isinstance(data, dict) else data return int(tags[0].get("count", 0)) if tags else 0 except Exception: return 0 def find_character_tag(selected_franchises, selected_hair): if not selected_franchises: return "⚠️ Error: Select a franchise", "Please select at least one franchise checkbox." chosen_franchise = random.choice(selected_franchises) tag_modifier = f"({chosen_franchise}" search_tags = f"1girl solo -alternate_hair_color -animated -1boy -*cosplay* *{tag_modifier}* score:>10 sort:random" if selected_hair != "Any": search_tags += f" {selected_hair}" log_buffer = f"🎲 Randomly selected franchise: {chosen_franchise}\n" log_buffer += f"🔎 Searching: {search_tags}\n" params = { "page": "dapi", "s": "post", "q": "index", "json": 1, "limit": 50, "tags": search_tags } if API_KEY and USER_ID: params["api_key"] = API_KEY params["user_id"] = USER_ID try: response = requests.get(ENDPOINT, params=params, headers=HEADERS) response.raise_for_status() data = response.json() posts = data.get("post",[]) if isinstance(data, dict) else data if not posts: log_buffer += "❌ No results found. Try selecting 'Any' hair color." return "No Match Found", log_buffer franchise_exceptions = FRANCHISE_EXCEPTIONS.get(chosen_franchise,[]) for post in posts: raw_tags = html.unescape(post.get("tags", "")) tag_list = raw_tags.split() franchise_tags =[ t for t in tag_list if tag_modifier in t and not any(u in t for u in UNWANTED) and not any(exc in t for exc in franchise_exceptions) ] if len(franchise_tags) == 1: candidate = franchise_tags[0] # Check popularity count = get_tag_count(candidate) time.sleep(0.2) if count >= 10: log_buffer += f"✅ Match found on Post ID {post.get('id')}\n" log_buffer += f"🏷️ Tag '{candidate}' has {count} posts." return candidate.replace('_', ' '), log_buffer else: log_buffer += f"⚠️ Candidate '{candidate}' only has {count} posts. Skipping.\n" log_buffer += "❌ Checked 50 posts but none matched criteria." return "No Match Found", log_buffer except Exception as e: return "API Error", f"An error occurred: {str(e)}" # --- UI HELPER FUNCTIONS --- def select_all(): return FRANCHISES_RAW def select_none(): return[] def invert_selection(current_selection): return[item for item in FRANCHISES_RAW if item not in current_selection] def copy_notification(x): if x and "Error" not in x and "No Match" not in x: gr.Info(f"Copied '{x}' to clipboard!") return x # --- GRADIO LAYOUT --- JS_COPY_LOGIC = """ (x) => { if (x && x !== "Result will appear here..." && !x.includes("Error")) { navigator.clipboard.writeText(x); } return x; } """ with gr.Blocks(theme=gr.themes.Soft()) as app: gr.Markdown("## 🏷️ Gelbooru Character Tag Finder") gr.Markdown("Pick your franchises and a hair color. The script will return **one specific character tag**.") with gr.Row(): # --- Left Column: Inputs --- with gr.Column(scale=1): franchise_input = gr.CheckboxGroup( choices=FRANCHISE_CHOICES, value=FRANCHISES_RAW, label="1. Select Franchises" ) with gr.Row(): btn_all = gr.Button("Select All", size="sm") btn_none = gr.Button("Clear All", size="sm") btn_invert = gr.Button("Invert Selection", size="sm") hair_input = gr.Radio( choices=HAIR_CHOICES, value="Any", label="2. Select Hair Color" ) submit_btn = gr.Button("Find Character Tag", variant="primary") # --- Right Column: Outputs --- with gr.Column(scale=1): gr.Markdown("### 👇 Click the button below to copy the tag") result_btn = gr.Button( value="Result will appear here...", variant="secondary", size="lg", interactive=True ) log_output = gr.TextArea( label="Process Logs", lines=10, max_lines=15, interactive=False ) # --- EVENT LISTENERS --- btn_all.click(fn=select_all, outputs=franchise_input) btn_none.click(fn=select_none, outputs=franchise_input) btn_invert.click(fn=invert_selection, inputs=franchise_input, outputs=franchise_input) submit_btn.click( fn=find_character_tag, inputs=[franchise_input, hair_input], outputs=[result_btn, log_output] ) result_btn.click( fn=copy_notification, inputs=result_btn, outputs=result_btn, js=JS_COPY_LOGIC ) if __name__ == "__main__": app.launch()