Download app.py from MuskAI/LTF-Demo: direct link, hf CLI and curl.
- Browser
- Download file 25.8 kB
-
https://huggingface.co/spaces/MuskAI/LTF-Demo/resolve/main/app.py
- Command line
-
hf download hf://spaces/MuskAI/LTF-Demo/app.py
-
curl -L -o app.py https://huggingface.co/spaces/MuskAI/LTF-Demo/resolve/main/app.py
25.8 kB
| """Original-file LTF inference for Hugging Face Spaces and local use.""" | |
| from __future__ import annotations | |
| import hashlib | |
| import json | |
| import logging | |
| import math | |
| import os | |
| from pathlib import Path | |
| import sys | |
| import threading | |
| import time | |
| # Gradio probes its own local HTTP server during startup. Keep local traffic | |
| # direct even on machines configured with outbound HTTP proxies. | |
| for key in ("NO_PROXY", "no_proxy"): | |
| existing = os.environ.get(key, "") | |
| os.environ[key] = ",".join( | |
| filter(None, [existing, "localhost", "127.0.0.1", "::1"]) | |
| ) | |
| import gradio as gr | |
| from huggingface_hub import snapshot_download | |
| from PIL import Image, UnidentifiedImageError | |
| import torch | |
| sys.path.insert(0, str(Path(__file__).absolute().parent)) | |
| from attention import ( | |
| capture_prediction, | |
| draw_grid, | |
| head_view, | |
| parameter_rows, | |
| patch_view, | |
| public_attention, | |
| tile_boxes, | |
| tile_overlay, | |
| top_tiles, | |
| ) | |
| from presentation import ( | |
| EMPTY_RESULT, | |
| HEADER, | |
| patch_matrix, | |
| result_card, | |
| stats_card, | |
| weight_chart, | |
| ) | |
| from sample_images import ( | |
| gallery_items, | |
| sample_at, | |
| annotate_example, | |
| same_image_as_state, | |
| ) | |
| from locale_settings import english_app_kwargs | |
| LOG = logging.getLogger(__name__) | |
| MAX_PIXELS = int(os.environ.get("LTF_MAX_PIXELS", "16000000")) | |
| MAX_VIEWS = int(os.environ.get("LTF_MAX_VIEWS", "400")) | |
| MODEL_LOCK = threading.Lock() | |
| _detector = None | |
| _model_revision = None | |
| def validate_upload(path): | |
| if not path: | |
| raise gr.Error("Please upload an image first.") | |
| source = Path(path) | |
| try: | |
| with Image.open(source) as image: | |
| width, height = image.size | |
| if getattr(image, "n_frames", 1) != 1: | |
| raise gr.Error("Please upload a single-frame image.") | |
| views = math.ceil(width / 224) * math.ceil(height / 224) + 1 | |
| if width * height > MAX_PIXELS or views > MAX_VIEWS: | |
| raise gr.Error( | |
| f"Image exceeds demo limits: {MAX_PIXELS:,} pixels " | |
| f"and {MAX_VIEWS} views. Please choose a smaller original image." | |
| ) | |
| image.verify() | |
| except (UnidentifiedImageError, OSError, ValueError, Image.DecompressionBombError): | |
| raise gr.Error("This file is not a readable image.") from None | |
| return source, width, height, views | |
| def resolve_model_dir(): | |
| local_dir = os.environ.get("LTF_MODEL_DIR") | |
| if local_dir: | |
| root = Path(local_dir).expanduser().resolve() | |
| if not root.is_dir(): | |
| raise RuntimeError("LTF_MODEL_DIR does not point to a model directory.") | |
| return root | |
| repo = os.environ.get("LTF_MODEL_REPO") | |
| if not repo: | |
| raise RuntimeError("Set LTF_MODEL_REPO or LTF_MODEL_DIR before inference.") | |
| return Path( | |
| snapshot_download( | |
| repo_id=repo, | |
| revision=os.environ.get("LTF_MODEL_REVISION", "main"), | |
| allow_patterns=[ | |
| "*.json", | |
| "configs/*", | |
| "weights/*", | |
| "backbones/*", | |
| "ltf_final/*.py", | |
| "model/*.py", | |
| "mil_module/*.py", | |
| "utils/*.py", | |
| ], | |
| ) | |
| ) | |
| def verify_model(root): | |
| manifest = json.loads((root / "release_manifest.json").read_text()) | |
| for relative, expected in manifest["files"].items(): | |
| relative_path = Path(relative) | |
| # Hub snapshot files are symlinks into the sibling blobs directory. | |
| # Validate the manifest path before following links, then hash the bytes. | |
| if relative_path.is_absolute() or ".." in relative_path.parts: | |
| raise RuntimeError("The model manifest contains an invalid path.") | |
| path = root / relative_path | |
| if not path.is_file(): | |
| raise RuntimeError("The model release is incomplete.") | |
| digest = hashlib.sha256() | |
| with path.open("rb") as stream: | |
| for block in iter(lambda: stream.read(8 * 1024 * 1024), b""): | |
| digest.update(block) | |
| if digest.hexdigest() != expected["sha256"]: | |
| raise RuntimeError(f"Model release checksum failed: {relative}") | |
| return manifest | |
| def get_detector(): | |
| """Called under MODEL_LOCK, so initialization and inference are serialized.""" | |
| global _detector, _model_revision | |
| if _detector is None: | |
| root = resolve_model_dir() | |
| verify_model(root) | |
| sys.path.insert(0, str(root)) | |
| from ltf_final import LTFDetector | |
| torch.set_num_threads(int(os.environ.get("LTF_CPU_THREADS", "2"))) | |
| _detector = LTFDetector( | |
| config=root / "configs/eval.yaml", | |
| weights=root / "weights/epoch_20.pth", | |
| device=os.environ.get("LTF_DEVICE", "auto"), | |
| chunk_size=int(os.environ.get("LTF_CHUNK_SIZE", "8")), | |
| ) | |
| _model_revision = os.environ.get("LTF_MODEL_REVISION", "local") | |
| return _detector | |
| def _predict_request(image_path, branch="fusion", threshold=0.5, with_attention=False): | |
| source, width, height, views = validate_upload(image_path) | |
| if branch not in ("fusion", "level1", "level2"): | |
| raise gr.Error("Unknown model branch.") | |
| if not math.isfinite(float(threshold)) or not 0 <= float(threshold) <= 1: | |
| raise gr.Error("Threshold must be between 0 and 1.") | |
| start = time.perf_counter() | |
| try: | |
| with MODEL_LOCK: | |
| detector = get_detector() | |
| if with_attention: | |
| result, attention = capture_prediction( | |
| detector, source, branch, float(threshold) | |
| ) | |
| state = dict( | |
| path=str(source), | |
| attention=attention, | |
| boxes=tile_boxes( | |
| width, height, int(detector.model.input_resolution) | |
| ), | |
| parameters=parameter_rows( | |
| detector, branch, float(threshold), views | |
| ), | |
| ) | |
| else: | |
| result = detector.predict(source, branch, float(threshold)) | |
| state = None | |
| except torch.cuda.OutOfMemoryError: | |
| torch.cuda.empty_cache() | |
| raise gr.Error( | |
| "GPU memory is insufficient. Please try a smaller image." | |
| ) from None | |
| except gr.Error: | |
| raise | |
| except Exception: | |
| LOG.exception("LTF inference failed") | |
| raise gr.Error( | |
| "Model inference failed. Check the Space runtime logs." | |
| ) from None | |
| # The preview never replaces the original image passed to the model. | |
| with Image.open(source) as original: | |
| preview = original.convert("RGB") | |
| preview.thumbnail((1024, 1024)) | |
| result.pop("path", None) | |
| result.update( | |
| width=width, | |
| height=height, | |
| model_revision=_model_revision, | |
| elapsed_seconds=round(time.perf_counter() - start, 3), | |
| ) | |
| assert result["views"] == views, "Preview and model tile counts disagree" | |
| scores = result["probabilities"][branch] | |
| summary = result_card(result) | |
| rows = [ | |
| [ | |
| name, | |
| result["probabilities"][name]["real"], | |
| result["probabilities"][name]["fake"], | |
| ] | |
| for name in ("fusion", "level1", "level2") | |
| ] | |
| if state: | |
| result["attention"] = public_attention(state) | |
| state["result"] = dict(result) | |
| return ( | |
| preview, | |
| summary, | |
| {"Real": scores["real"], "AI-generated": scores["fake"]}, | |
| rows, | |
| result, | |
| ), state | |
| def predict(image_path, branch="fusion", threshold=0.5): | |
| """Keep the original five-output public prediction API compatible.""" | |
| return _predict_request(image_path, branch, threshold)[0] | |
| def explain(image_path, branch="fusion", threshold=0.5): | |
| base, state = _predict_request(image_path, branch, threshold, with_attention=True) | |
| attention = state["attention"] | |
| count = len(state["boxes"]) | |
| if len(attention["l1"]) != count or len(attention["l2"]) != count + 1: | |
| raise gr.Error("Attention and original image tile counts disagree.") | |
| grid = draw_grid(state["path"], state["boxes"]) | |
| l1_image, l1_description = head_view(state, "mean") | |
| l2_image = tile_overlay( | |
| state["path"], state["boxes"], attention["l2"][1:], reference_count=count + 1 | |
| ) | |
| description = ( | |
| f'Global view weight **{attention["l2"][0]*100:.2f}%**; total local view weight ' | |
| f'**{float(attention["l2"][1:].sum())*100:.2f}%**. ' | |
| 'The global view is listed separately; the overlay shows local tiles only.\n\n' | |
| f'Feature fusion coefficients: L1 脳 {attention["gate"]:.4f}, L2 脳 {1-attention["gate"]:.4f}. ' | |
| 'These coefficients scale branch embeddings, not branch probabilities.' | |
| ) | |
| weight_rows = [["Global", "whole image", None, float(attention["l2"][0])]] | |
| weight_rows.extend( | |
| [ | |
| f"T{i + 1}", | |
| str(list(box)), | |
| float(attention["l1"][i]), | |
| float(attention["l2"][i + 1]), | |
| ] | |
| for i, box in enumerate(state["boxes"]) | |
| ) | |
| selected = int(attention["l1"].argmax()) + 1 | |
| tile, patch, patch_description = patch_view(state, str(selected)) | |
| return ( | |
| *base, | |
| grid, | |
| l1_image, | |
| l1_description, | |
| l2_image, | |
| description, | |
| top_tiles(state), | |
| state["parameters"], | |
| weight_rows, | |
| state, | |
| gr.Dropdown(choices=[str(i + 1) for i in range(count)], value=str(selected)), | |
| tile, | |
| patch, | |
| patch_description, | |
| gr.Dropdown( | |
| choices=[("Mean (8 Heads)", "mean")] | |
| + [ | |
| (f"Head {i + 1}", f"head_{i + 1}") | |
| for i in range(attention["l1_heads"].shape[0]) | |
| ], | |
| value="mean", | |
| ), | |
| weight_chart(state), | |
| weight_chart(state, level="l2"), | |
| patch_matrix(state, str(selected)), | |
| stats_card(state), | |
| gr.update(visible=True), | |
| ) | |
| def inspect_head(state, selection): | |
| image, description = head_view(state, selection) | |
| return image, description, weight_chart(state, selection) | |
| def inspect_patch(state, tile_number): | |
| tile, image, description = patch_view(state, tile_number) | |
| return tile, image, description, patch_matrix(state, tile_number) | |
| def select_top_tile(state, event: gr.SelectData): | |
| if not state: | |
| return gr.skip() | |
| index = event.index[0] if isinstance(event.index, (tuple, list)) else event.index | |
| order = sorted( | |
| range(len(state["boxes"])), key=lambda i: -float(state["attention"]["l1"][i]) | |
| )[:4] | |
| return str(order[int(index)] + 1) | |
| def update_decision(state, branch, threshold): | |
| """Threshold and branch selection use cached logits, without another forward.""" | |
| if not state: | |
| return tuple(gr.skip() for _ in range(5)) | |
| result = dict(state["result"]) | |
| score = result["probabilities"][branch]["fake"] | |
| label = int(score >= float(threshold)) | |
| result.update( | |
| branch=branch, | |
| threshold=float(threshold), | |
| score_fake=score, | |
| label=label, | |
| prediction="fake" if label else "real", | |
| ) | |
| scores = result["probabilities"][branch] | |
| rows = [ | |
| [ | |
| name, | |
| result["probabilities"][name]["real"], | |
| result["probabilities"][name]["fake"], | |
| ] | |
| for name in ("fusion", "level1", "level2") | |
| ] | |
| parameters = [list(row) for row in state["parameters"]] | |
| parameters[0][2] = f"{branch} / {float(threshold):.2f}" | |
| return ( | |
| result_card(result), | |
| {"Real": scores["real"], "AI-generated": scores["fake"]}, | |
| rows, | |
| result, | |
| parameters, | |
| ) | |
| def reset_upload(path): | |
| image = None | |
| if path: | |
| try: | |
| source, *_ = validate_upload(path) | |
| with Image.open(source) as original: | |
| image = original.convert("RGB") | |
| image.thumbnail((1024, 1024)) | |
| except gr.Error: | |
| pass | |
| return image, EMPTY_RESULT, None, [], None, [], None, gr.update(visible=False) | |
| def run_example(branch, threshold, event: gr.SelectData): | |
| sample, path = sample_at(event.index) | |
| outputs = explain(str(path), branch, threshold) | |
| description = annotate_example(outputs, sample) | |
| return (*outputs, str(path), description) | |
| def on_upload_change(path, current_state): | |
| if same_image_as_state(path, current_state): | |
| return tuple(gr.skip() for _ in range(9)) | |
| return (*reset_upload(path), "") | |
| def build_demo(): | |
| with gr.Blocks( | |
| title="LTF 路 Image Detection and Attention Analysis", | |
| fill_width=True, | |
| delete_cache=(3600, 3600), | |
| ) as demo: | |
| gr.HTML(HEADER) | |
| with gr.Row(): | |
| with gr.Column(scale=3, min_width=260, elem_id="upload-panel"): | |
| gr.Markdown( | |
| "### 01 路 Upload an image\nInference uses the original file. Resizing the preview does not change the model input." | |
| ) | |
| upload = gr.File( | |
| label="Image file", | |
| type="filepath", | |
| file_count="single", | |
| file_types=["image"], | |
| height=150, | |
| ) | |
| clear_upload = gr.ClearButton([upload], value="Change image", size="sm") | |
| branch = gr.Dropdown( | |
| [ | |
| ("Fusion 路 Combined branches", "fusion"), | |
| ("L1 路 Patch-level branch", "level1"), | |
| ("L2 路 View-level branch", "level2"), | |
| ], | |
| value="fusion", | |
| label="Prediction branch", | |
| elem_id="branch-select", | |
| ) | |
| threshold = gr.Slider( | |
| 0, | |
| 1, | |
| value=0.5, | |
| step=0.01, | |
| label="Decision threshold", | |
| elem_id="threshold-select", | |
| ) | |
| button = gr.Button( | |
| "Run Analysis", variant="primary", elem_id="run-button" | |
| ) | |
| gr.Markdown( | |
| f"Limits: {MAX_PIXELS/1_000_000:g} MP 路 {MAX_VIEWS} views 路 30 MB. Large images take longer on CPU." | |
| ) | |
| with gr.Column(scale=4, min_width=300): | |
| preview = gr.Image( | |
| label="Original image preview", | |
| height=390, | |
| interactive=False, | |
| buttons=["download", "fullscreen"], | |
| elem_id="original-preview", | |
| ) | |
| with gr.Column(scale=4, min_width=300): | |
| summary = gr.HTML(EMPTY_RESULT, label="Analysis result") | |
| scores = gr.Label( | |
| label="Model scores", num_top_classes=2, visible=False | |
| ) | |
| with gr.Column(elem_id="example-section"): | |
| gr.Markdown( | |
| "### GenImage examples 路 Click an image to analyze\nReal / Generated captions are dataset labels. Model predictions and attention appear in the analysis panels." | |
| ) | |
| example_gallery = gr.Gallery( | |
| value=gallery_items(), | |
| columns=4, | |
| rows=2, | |
| height="auto", | |
| label="GenImage validation examples", | |
| show_label=False, | |
| object_fit="contain", | |
| allow_preview=False, | |
| preview=False, | |
| interactive=False, | |
| buttons=[], | |
| elem_id="example-gallery", | |
| ) | |
| example_info = gr.Markdown("", elem_id="example-info") | |
| state = gr.State(value=None, time_to_live=3600) | |
| with gr.Column(visible=False, elem_id="analysis-section") as analysis: | |
| statistics = gr.HTML() | |
| gr.Markdown( | |
| "## 02 路 Attention Analysis\nAll heatmaps use the same uniform-attention reference. Nearly uniform weights have similar colors. Attention is not a forgery probability." | |
| ) | |
| with gr.Tabs(selected="l1", elem_id="analysis-tabs"): | |
| with gr.Tab("L1 路 Tile Attention", id="l1"): | |
| with gr.Row(): | |
| with gr.Column(scale=7, min_width=320): | |
| l1_image = gr.Image( | |
| label="L1 tile attention overlay", | |
| height=500, | |
| interactive=False, | |
| buttons=["download", "fullscreen"], | |
| elem_classes="attention-image", | |
| ) | |
| l1_description = gr.Markdown() | |
| with gr.Column(scale=5, min_width=300): | |
| head = gr.Dropdown( | |
| [("Mean (8 Heads)", "mean")], | |
| value="mean", | |
| label="Aggregation head", | |
| elem_id="head-select", | |
| ) | |
| l1_chart = gr.HTML() | |
| with gr.Tab("L2 路 View Weights", id="l2"): | |
| with gr.Row(): | |
| with gr.Column(scale=7, min_width=320): | |
| l2_image = gr.Image( | |
| label="L2 local view attention overlay", | |
| height=500, | |
| interactive=False, | |
| buttons=["download", "fullscreen"], | |
| elem_classes="attention-image", | |
| ) | |
| attention_description = gr.Markdown() | |
| with gr.Column(scale=5, min_width=300): | |
| l2_chart = gr.HTML() | |
| with gr.Tab("Original 路 Tile Layout", id="grid"): | |
| grid = gr.Image( | |
| label="Original 224 脳 224 tile boundaries", | |
| height=570, | |
| interactive=False, | |
| buttons=["download", "fullscreen"], | |
| elem_classes="attention-image", | |
| ) | |
| gr.Markdown( | |
| "Tiles are numbered row by row. Full 224-pixel blocks do not overlap; only the final right/bottom boundary blocks may overlap. Some labels are omitted on dense grids; complete coordinates are listed below." | |
| ) | |
| with gr.Tab("Tile 路 Patch Details", id="patch"): | |
| gr.Markdown( | |
| "### Top 4 tiles by L1 attention\nClick a tile to inspect its original crop, attention overlay, and patch weights. The top-four selection is for display only and does not affect prediction." | |
| ) | |
| gallery = gr.Gallery( | |
| label="High-attention tiles", | |
| columns=4, | |
| object_fit="contain", | |
| height=250, | |
| elem_id="top-crops", | |
| show_label=False, | |
| preview=False, | |
| allow_preview=False, | |
| interactive=False, | |
| buttons=["download_all"], | |
| ) | |
| tile_number = gr.Dropdown( | |
| choices=[], | |
| label="Tile number", | |
| allow_custom_value=False, | |
| elem_id="tile-select", | |
| ) | |
| with gr.Row(): | |
| with gr.Column(min_width=250): | |
| selected_tile = gr.Image( | |
| label="Original 224 脳 224 crop", | |
| height=330, | |
| interactive=False, | |
| buttons=["download", "fullscreen"], | |
| ) | |
| with gr.Column(min_width=250): | |
| selected_patch = gr.Image( | |
| label="Patch attention overlay", | |
| height=330, | |
| interactive=False, | |
| buttons=["download", "fullscreen"], | |
| ) | |
| with gr.Column(min_width=280): | |
| patch_chart = gr.HTML() | |
| patch_description = gr.Markdown() | |
| with gr.Accordion( | |
| "Exact weights, parameters, and raw output", | |
| open=False, | |
| elem_id="technical-details", | |
| ): | |
| gr.Markdown("### Branch scores") | |
| table = gr.Dataframe( | |
| headers=["Branch", "Real score", "Generated score"], | |
| datatype=["str", "number", "number"], | |
| interactive=False, | |
| ) | |
| gr.Markdown( | |
| "### View weights\nL1 is the mean across eight heads. The first L2 entry represents the separate global view." | |
| ) | |
| weight_table = gr.Dataframe( | |
| headers=["View", "Original crop box", "L1 mean weight", "L2 weight"], | |
| datatype=["str", "str", "number", "number"], | |
| interactive=False, | |
| ) | |
| gr.Markdown( | |
| "### Model and hyperparameters\nTraining-only rows record the historical configuration. Random augmentation, Cross-Shuffle, and dropout are disabled during inference." | |
| ) | |
| parameters = gr.Dataframe( | |
| headers=["Scope", "Parameter", "Value"], | |
| datatype=["str", "str", "str"], | |
| interactive=False, | |
| ) | |
| details = gr.JSON(label="Prediction JSON") | |
| analysis_outputs = [ | |
| preview, | |
| summary, | |
| scores, | |
| table, | |
| details, | |
| grid, | |
| l1_image, | |
| l1_description, | |
| l2_image, | |
| attention_description, | |
| gallery, | |
| parameters, | |
| weight_table, | |
| state, | |
| tile_number, | |
| selected_tile, | |
| selected_patch, | |
| patch_description, | |
| head, | |
| l1_chart, | |
| l2_chart, | |
| patch_chart, | |
| statistics, | |
| analysis, | |
| ] | |
| example_event = example_gallery.select( | |
| run_example, | |
| [branch, threshold], | |
| analysis_outputs + [upload, example_info], | |
| api_name=False, | |
| concurrency_limit=1, | |
| concurrency_id="ltf-inference", | |
| trigger_mode="always_last", | |
| scroll_to_output=True, | |
| ) | |
| run_event = button.click( | |
| explain, | |
| [upload, branch, threshold], | |
| analysis_outputs, | |
| api_name="explain", | |
| concurrency_limit=1, | |
| concurrency_id="ltf-inference", | |
| cancels=[example_event], | |
| ) | |
| example_gallery.select(fn=None, cancels=[run_event], queue=False) | |
| upload.upload(fn=None, cancels=[example_event], queue=False) | |
| clear_upload.click(fn=None, cancels=[example_event], queue=False) | |
| head.change( | |
| inspect_head, | |
| [state, head], | |
| [l1_image, l1_description, l1_chart], | |
| api_name=False, | |
| ) | |
| tile_number.change( | |
| inspect_patch, | |
| [state, tile_number], | |
| [selected_tile, selected_patch, patch_description, patch_chart], | |
| api_name=False, | |
| ) | |
| gallery.select(select_top_tile, [state], [tile_number], api_name=False) | |
| for control in (branch, threshold): | |
| control.change( | |
| update_decision, | |
| [state, branch, threshold], | |
| [summary, scores, table, details, parameters], | |
| api_name=False, | |
| queue=False, | |
| trigger_mode="always_last", | |
| ) | |
| upload.change( | |
| on_upload_change, | |
| [upload, state], | |
| [ | |
| preview, | |
| summary, | |
| scores, | |
| table, | |
| details, | |
| parameters, | |
| state, | |
| analysis, | |
| example_info, | |
| ], | |
| api_name=False, | |
| queue=False, | |
| cancels=[run_event], | |
| ) | |
| api_button = gr.Button(visible=False) | |
| api_button.click( | |
| predict, | |
| [upload, branch, threshold], | |
| [preview, summary, scores, table, details], | |
| api_name="predict", | |
| concurrency_limit=1, | |
| concurrency_id="ltf-inference", | |
| ) | |
| gr.Markdown( | |
| "LTF 路 Original checkpoint and inference pipeline 路 Visualization preserves model scores", | |
| elem_id="footer-note", | |
| ) | |
| return demo.queue(max_size=16, default_concurrency_limit=1) | |
| if __name__ == "__main__": | |
| logging.basicConfig(level=logging.INFO) | |
| build_demo().launch( | |
| server_name=os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"), | |
| server_port=int(os.environ.get("PORT", "7860")), | |
| share=False, | |
| max_file_size="30mb", | |
| app_kwargs=english_app_kwargs(), | |
| ssr_mode=False, | |
| footer_links=["api"], | |
| theme=gr.themes.Soft( | |
| primary_hue="teal", | |
| neutral_hue="slate", | |
| font=["ui-sans-serif", "system-ui", "sans-serif"], | |
| ).set(block_label_background_fill="#eef3f8", block_label_text_color="#445b72"), | |
| css_paths=Path(__file__).with_name("styles.css"), | |
| ) | |