CoolFace
Apppublic

tlam/metadata

sourceHugging Faceapache-2.0updated 4mo agoView on Hugging Face
3likes
app.py440 linesDownload Raw Back to root
1import gradio as gr2from PIL import Image3from PIL.ExifTags import TAGS4import re5import json6import tempfile7import os8import math9 10 11def get_aspect_ratio(w, h):12    """Return the closest standard aspect ratio string for given dimensions."""13    if w == 0 or h == 0:14        return "N/A"15    ratio = w / h16    tolerance = 0.0217    ratios = {18        "1:1": 1.0,19        "5:4": 5 / 4,20        "4:3": 4 / 3,21        "3:2": 3 / 2,22        "16:9": 16 / 9,23        "16:10": 16 / 10,24        "21:9": 21 / 9,25        "2:3": 2 / 3,26        "3:4": 3 / 4,27        "4:5": 4 / 5,28        "9:16": 9 / 16,29    }30    for label, val in ratios.items():31        if abs(ratio - val) < tolerance:32            return label33    gcd = math.gcd(w, h)34    return f"{w // gcd}:{h // gcd}"35 36 37def get_image_metadata(img):38    """Extract metadata based on image format."""39    metadata = {}40    if img.format == "PNG":41        metadata = img.info42    elif img.format in ["JPEG", "TIFF", "WEBP"]:43        exif_data = img._getexif()44        if exif_data:45            for tag, value in exif_data.items():46                tag_name = TAGS.get(tag, tag)47                metadata[tag_name] = value48    return metadata49 50 51def parse_comfy_prompt(prompt_data):52    """Parse ComfyUI prompt JSON using graph tracing for correct pos/neg identification."""53    positive_prompt = "N/A"54    negative_prompt = "N/A"55    seed = "N/A"56 57    try:58        if isinstance(prompt_data, str):59            prompt_data = json.loads(prompt_data)60    except (json.JSONDecodeError, TypeError):61        return positive_prompt, negative_prompt, seed, []62 63    if not isinstance(prompt_data, dict):64        return positive_prompt, negative_prompt, seed, []65 66    clip_text_nodes = {}67    lora_names = []68 69    for node_id, node in prompt_data.items():70        if not isinstance(node, dict):71            continue72        class_type = node.get("class_type", "")73        inputs = node.get("inputs", {})74        widgets = node.get("widgets_values", [])75 76        if class_type in ("CLIPTextEncode", "CLIPTextEncodeSDXL"):77            text = inputs.get("text", "")78            if isinstance(text, list):79                text = ""80            if not text or not text.strip():81                continue82            clip_text_nodes[node_id] = {83                "text": text.strip(),84                "links": node.get("inputs", []),85            }86 87        if "LoraLoader" in class_type or "LoraLoaderModelOnly" in class_type:88            lora_name = inputs.get("lora_name", inputs.get("lora", ""))89            if isinstance(lora_name, str) and lora_name.strip():90                lora_names.append(lora_name.strip())91            for wv in widgets:92                if isinstance(wv, str) and wv.endswith(93                    (".safetensors", ".ckpt", ".pt")94                ):95                    if wv not in lora_names:96                        lora_names.append(wv)97 98        if class_type in (99            "KSampler",100            "KSamplerAdvanced",101            "SamplerCustom",102            "SamplerCustomAdvanced",103        ):104            if "seed" in inputs:105                seed = str(inputs["seed"])106            if "noise_seed" in inputs:107                seed = str(inputs["noise_seed"])108 109    positive_ids = set()110    negative_ids = set()111 112    for node_id, node in prompt_data.items():113        if not isinstance(node, dict):114            continue115        class_type = node.get("class_type", "")116        inputs = node.get("inputs", {})117 118        is_sampler = class_type in (119            "KSampler",120            "KSamplerAdvanced",121            "SamplerCustom",122            "SamplerCustomAdvanced",123        )124        if is_sampler:125            for input_name in ("positive", "latent_image"):126                link = inputs.get(input_name)127                if isinstance(link, list) and len(link) >= 1:128                    positive_ids.add(str(link[0]))129            for input_name in ("negative",):130                link = inputs.get(input_name)131                if isinstance(link, list) and len(link) >= 1:132                    negative_ids.add(str(link[0]))133 134        if class_type in ("SamplerCustom", "SamplerCustomAdvanced"):135            for input_name in ("guider", "sampler", "sigmas"):136                link = inputs.get(input_name)137                if isinstance(link, list) and len(link) >= 1:138                    pass139 140    def trace_positive(node_id, visited=None):141        if visited is None:142            visited = set()143        if node_id in visited:144            return []145        visited.add(node_id)146        node = prompt_data.get(node_id, {})147        if not isinstance(node, dict):148            return []149        class_type = node.get("class_type", "")150        inputs = node.get("inputs", {})151        texts = []152        if class_type in ("CLIPTextEncode", "CLIPTextEncodeSDXL"):153            text = inputs.get("text", "")154            if isinstance(text, list):155                text = ""156            if text and text.strip():157                texts.append(text.strip())158        for input_name, link in inputs.items():159            if isinstance(link, list) and len(link) >= 1:160                texts.extend(trace_positive(str(link[0]), visited))161        return texts162 163    def trace_negative(node_id, visited=None):164        if visited is None:165            visited = set()166        if node_id in visited:167            return []168        visited.add(node_id)169        node = prompt_data.get(node_id, {})170        if not isinstance(node, dict):171            return []172        class_type = node.get("class_type", "")173        inputs = node.get("inputs", {})174        texts = []175        if class_type in ("CLIPTextEncode", "CLIPTextEncodeSDXL"):176            text = inputs.get("text", "")177            if isinstance(text, list):178                text = ""179            if text and text.strip():180                texts.append(text.strip())181        for input_name, link in inputs.items():182            if isinstance(link, list) and len(link) >= 1:183                texts.extend(trace_negative(str(link[0]), visited))184        return texts185 186    found_pos = False187    found_neg = False188    for nid in sorted(positive_ids):189        if nid in clip_text_nodes:190            positive_prompt = clip_text_nodes[nid]["text"]191            found_pos = True192            break193    if not found_pos:194        for nid in sorted(positive_ids):195            texts = trace_positive(nid)196            if texts:197                positive_prompt = texts[0]198                found_pos = True199                break200 201    for nid in sorted(negative_ids):202        if nid in clip_text_nodes:203            negative_prompt = clip_text_nodes[nid]["text"]204            found_neg = True205            break206    if not found_neg:207        for nid in sorted(negative_ids):208            texts = trace_negative(nid)209            if texts:210                negative_prompt = texts[0]211                found_neg = True212                break213 214    if not found_pos and not found_neg:215        texts = [n["text"] for n in clip_text_nodes.values()]216        if len(texts) >= 1:217            positive_prompt = texts[0]218        if len(texts) >= 2:219            negative_prompt = texts[1]220 221    return positive_prompt, negative_prompt, seed, lora_names222 223 224def extract_metadata(image_file):225    """Extract and parse metadata from the uploaded image."""226    try:227        img = Image.open(image_file)228    except Exception:229        return ("Error: Unable to open image.", *(["N/A"] * 7))230 231    metadata = get_image_metadata(img)232    width, height = img.size233    aspect_ratio = get_aspect_ratio(width, height)234    dimensions = f"{width} × {height} ({aspect_ratio})"235 236    metadata_str = "\n".join([f"{key}: {value}" for key, value in metadata.items()])237 238    prompt = "N/A"239    negative_prompt = "N/A"240    seed_number = "N/A"241    comfy_workflow = "N/A"242    lora_names = []243 244    comfy_prompt_data = metadata.get("prompt", None)245    if comfy_prompt_data is not None:246        pos, neg, s, loras = parse_comfy_prompt(comfy_prompt_data)247        if pos != "N/A":248            prompt = pos249        if neg != "N/A":250            negative_prompt = neg251        if s != "N/A":252            seed_number = s253        lora_names = loras254 255    elif "parameters" in metadata:256        params = metadata["parameters"]257        prompt_match = re.search(258            r"(.*?)Negative prompt:", params, re.DOTALL | re.IGNORECASE259        )260        if prompt_match:261            prompt = prompt_match.group(1).strip()262        else:263            prompt_match = re.search(264                r"(.*?)(Steps:|$)", params, re.DOTALL | re.IGNORECASE265            )266            if prompt_match:267                prompt = prompt_match.group(1).strip()268 269        neg_match = re.search(270            r"Negative prompt:(.*?)(Steps:|$)", params, re.DOTALL | re.IGNORECASE271        )272        if neg_match:273            negative_prompt = neg_match.group(1).strip()274 275        seed_match = re.search(r"Seed:\s*(\d+)", params, re.IGNORECASE)276        if seed_match:277            seed_number = seed_match.group(1).strip()278 279        lora_pattern = re.compile(r"<lora:([^:>]+)", re.IGNORECASE)280        lora_names = lora_pattern.findall(params)281 282    workflow_data = metadata.get("workflow", None)283    if workflow_data is not None:284        if isinstance(workflow_data, str):285            try:286                json.loads(workflow_data)287                comfy_workflow = workflow_data288            except json.JSONDecodeError:289                comfy_workflow = workflow_data290        elif isinstance(workflow_data, dict):291            comfy_workflow = json.dumps(workflow_data, indent=2)292        else:293            comfy_workflow = str(workflow_data)294 295    return (296        dimensions,297        prompt,298        negative_prompt,299        seed_number,300        metadata_str,301        comfy_workflow,302        lora_names,303        "N/A",304    )305 306 307def export_workflow(workflow_text):308    """Convert the workflow text into a downloadable JSON file."""309    if workflow_text == "N/A" or not workflow_text.strip():310        return None, "No workflow data to export."311 312    workflow_data = {"comfy_workflow": workflow_text}313 314    try:315        with tempfile.NamedTemporaryFile(316            mode="w", delete=False, suffix=".json"317        ) as tmp_file:318            json.dump(workflow_data, tmp_file, indent=4)319            tmp_file_path = tmp_file.name320    except Exception:321        return None, "Failed to create workflow JSON file."322 323    if os.path.exists(tmp_file_path):324        return tmp_file_path, "Workflow exported successfully."325    else:326        return None, "Failed to export workflow."327 328 329def strip_metadata(image_file):330    """Strip all metadata from an image and return a clean file."""331    if image_file is None:332        return None, "No image provided."333 334    try:335        img = Image.open(image_file)336    except Exception:337        return None, "Error: Unable to open image."338 339    clean_img = Image.new(img.mode, img.size)340    clean_img.putdata(list(img.getdata()))341 342    try:343        with tempfile.NamedTemporaryFile(344            mode="wb", delete=False, suffix=".png"345        ) as tmp_file:346            clean_img.save(tmp_file, format="PNG")347            tmp_file_path = tmp_file.name348    except Exception:349        return None, "Error saving stripped image."350 351    return tmp_file_path, "Metadata stripped successfully."352 353 354def main():355    with gr.Blocks() as iface:356        gr.Markdown("<h1>Comfy / A1111 Metadata Reader</h1>")357        gr.Markdown(358            "<p>Upload an image (PNG, JPEG, WebP) to extract its metadata and parse it for prompts.</p>"359        )360        with gr.Row():361            with gr.Column(scale=1):362                image_input = gr.Image(label="Drop Image Here", type="filepath")363 364            with gr.Column(scale=2):365                dimensions_output = gr.Textbox(366                    label="Dimensions", lines=1, interactive=False367                )368                prompt_output = gr.Textbox(369                    label="Prompt", lines=4, show_copy_button=True, interactive=False370                )371                negative_prompt_output = gr.Textbox(372                    label="Negative Prompt",373                    lines=4,374                    show_copy_button=True,375                    interactive=False,376                )377                seed_output = gr.Textbox(378                    label="Seed Number",379                    lines=1,380                    show_copy_button=True,381                    interactive=False,382                )383                lora_output = gr.Textbox(384                    label="LoRA(s)", lines=2, show_copy_button=True, interactive=False385                )386                original_metadata_output = gr.Textbox(387                    label="Original Metadata", lines=15, interactive=False388                )389 390            with gr.Column(scale=2):391                comfy_workflow_output = gr.Textbox(392                    label="Comfy Workflow", lines=20, value="N/A", interactive=False393                )394                with gr.Row():395                    export_button = gr.Button("Export Workflow as JSON")396                    strip_button = gr.Button("Strip Metadata", variant="stop")397                workflow_file = gr.File(label="Download Workflow JSON", visible=False)398                export_message = gr.Textbox(399                    label="Export Status", lines=1, interactive=False400                )401                strip_file = gr.File(label="Download Stripped Image", visible=False)402                strip_message = gr.Textbox(403                    label="Strip Status", lines=1, interactive=False404                )405 406        image_input.change(407            fn=extract_metadata,408            inputs=image_input,409            outputs=[410                dimensions_output,411                prompt_output,412                negative_prompt_output,413                seed_output,414                original_metadata_output,415                comfy_workflow_output,416                lora_output,417                strip_message,418            ],419        )420 421        export_button.click(422            fn=export_workflow,423            inputs=comfy_workflow_output,424            outputs=[workflow_file, export_message],425            queue=False,426        )427 428        strip_button.click(429            fn=strip_metadata,430            inputs=image_input,431            outputs=[strip_file, strip_message],432            queue=False,433        )434 435    iface.launch()436 437 438if __name__ == "__main__":439    main()440