Spaces:
Sleeping
Sleeping
| 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() |