Image-Text-to-Text
Transformers
English
Chinese
eviscene
multimodal
image-factuality
agent
sft
distillation
Instructions to use ZiAnwang/EviScene with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use ZiAnwang/EviScene with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="ZiAnwang/EviScene")# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("ZiAnwang/EviScene", device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use ZiAnwang/EviScene with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "ZiAnwang/EviScene" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "ZiAnwang/EviScene", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/ZiAnwang/EviScene
- SGLang
How to use ZiAnwang/EviScene with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "ZiAnwang/EviScene" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "ZiAnwang/EviScene", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "ZiAnwang/EviScene" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "ZiAnwang/EviScene", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Docker Model Runner
How to use ZiAnwang/EviScene with Docker Model Runner:
docker model run hf.co/ZiAnwang/EviScene
Download eviscene/tools/compare_reference.py from ZiAnwang/EviScene: direct link, hf CLI and curl.
- Browser
- Download file 44.4 kB
-
https://huggingface.co/ZiAnwang/EviScene/resolve/main/eviscene/tools/compare_reference.py
- Command line
-
hf download hf://ZiAnwang/EviScene/eviscene/tools/compare_reference.py
-
curl -L -o compare_reference.py https://huggingface.co/ZiAnwang/EviScene/resolve/main/eviscene/tools/compare_reference.py
44.4 kB
| # -*- coding: utf-8 -*- | |
| """Compare the current image with one reference image using a structured VLM.""" | |
| from __future__ import annotations | |
| import asyncio | |
| import os | |
| import re | |
| import base64 | |
| import io | |
| import threading | |
| import time | |
| from collections import OrderedDict | |
| from dataclasses import dataclass, field | |
| from typing import Any, Dict, Mapping, Optional | |
| from urllib.parse import parse_qsl, urlencode, urljoin, urlsplit, urlunsplit | |
| from eviscene.integrations.gemini import ( | |
| RUNTIME_METRICS_KEY, | |
| exception_runtime_metrics, | |
| ) | |
| from eviscene.integrations.http_sessions import ( | |
| close_response, | |
| close_tracked_sessions, | |
| get_tracked_session, | |
| init_tracked_sessions, | |
| ) | |
| from eviscene.tools.base import BaseTool | |
| from eviscene.orchestrator.evidence_semantics import derive_edit_evidence_summary | |
| from eviscene.orchestrator.source_access import SourceAccessPolicy | |
| DIFFERENCE_TYPES = ( | |
| "crop", | |
| "color_adjust", | |
| "watermark", | |
| "perspective", | |
| "lighting", | |
| "compression", | |
| "occlusion", | |
| "addition", | |
| "removal", | |
| "modification", | |
| "different_capture", | |
| "unrelated_content", | |
| "uncertain", | |
| ) | |
| EDIT_DIFFERENCE_TYPES = frozenset({"addition", "removal", "modification"}) | |
| EDIT_STRENGTHS = ("none", "weak", "moderate", "strong") | |
| SIGNIFICANCE_LEVELS = ("high", "medium", "low") | |
| DEFAULT_REFERENCE_COMPARE_MAX_OUTPUT_TOKENS = 8192 | |
| COMPARE_RESPONSE_SCHEMA: Dict[str, Any] = { | |
| "type": "object", | |
| "properties": { | |
| "same_subject_or_scene": {"type": "boolean"}, | |
| "same_capture_or_near_duplicate": {"type": "boolean"}, | |
| "likely_different_original_capture": {"type": "boolean"}, | |
| "differences": { | |
| "type": "array", | |
| "maxItems": 20, | |
| "items": { | |
| "type": "object", | |
| "properties": { | |
| "region": {"type": "string", "maxLength": 300}, | |
| "description": {"type": "string", "maxLength": 800}, | |
| "type": {"type": "string", "enum": list(DIFFERENCE_TYPES)}, | |
| "significance": { | |
| "type": "string", | |
| "enum": list(SIGNIFICANCE_LEVELS), | |
| }, | |
| }, | |
| }, | |
| }, | |
| "overall_observation": {"type": "string", "maxLength": 1200}, | |
| "confidence": {"type": "number", "minimum": 0.0, "maximum": 1.0}, | |
| }, | |
| } | |
| COMPARE_PROMPT = """\ | |
| Compare the two supplied images for evidence extraction, not final fact-check judgment. | |
| Image 1 is a candidate reference image found through search. | |
| Image 2 is the current image being verified. | |
| Focus: {focus} | |
| Describe concrete visual relationships and differences only. | |
| 1. Decide whether the images show the same subject, event, object, or scene. | |
| 2. Distinguish the same original capture or a near-duplicate from different original captures. | |
| 3. Use difference type addition, removal, or modification only for directly visible | |
| edit evidence. The runtime derives both the edit-evidence flag and its strength | |
| from these difference types and their significance; do not output either field. | |
| 4. Treat crop, resizing, compression, lighting, perspective, watermark, occlusion, and | |
| color shifts as benign unless they clearly alter factual content. | |
| 5. If the images are unrelated, report unrelated content without inferring manipulation. | |
| 6. Explain uncertainty in the observation rather than guessing manipulation. | |
| """ | |
| SYSTEM_INSTRUCTION = ( | |
| "You are an image comparison analyst. Return exactly one JSON object matching " | |
| "the supplied response schema, without markdown or commentary." | |
| ) | |
| class CompareWithReferenceTool(BaseTool): | |
| """Compare the current image with a reference image for visual evidence.""" | |
| name: str = "compare_with_reference" | |
| description: str = ( | |
| "Compare the current image with a reference image found via search and return " | |
| "low-level evidence about whether the images are near-duplicates, different " | |
| "original captures, or show direct edit evidence." | |
| ) | |
| parameters: Dict[str, Any] = field(default_factory=lambda: { | |
| "type": "object", | |
| "properties": { | |
| "reference_url": { | |
| "type": "string", | |
| "description": "URL of the reference image to compare against.", | |
| }, | |
| "focus": { | |
| "type": "string", | |
| "description": ( | |
| "What to focus on in the comparison. For example: " | |
| "'number of windows', 'person on the left', or 'banner text'." | |
| ), | |
| }, | |
| "source_page_url": { | |
| "type": "string", | |
| "description": ( | |
| "Optional surrounding page URL used only to recover a blocked " | |
| "or expired direct image URL." | |
| ), | |
| }, | |
| "visual_question_id": {"type": "string"}, | |
| "source_evidence_id": {"type": "string"}, | |
| "source_discovery_id": {"type": "string"}, | |
| "expected_property": {"type": "string"}, | |
| }, | |
| "required": ["reference_url"], | |
| }) | |
| client: Any = None | |
| provider: str = "gemini" | |
| model_name: str = "gemini-3.7-flash" | |
| image_path: str = "" | |
| source_access_policy: Any = None | |
| _reference_cache: OrderedDict[str, tuple[float, Dict[str, Any], int]] = field( | |
| default_factory=OrderedDict, | |
| init=False, | |
| repr=False, | |
| ) | |
| _reference_cache_bytes: int = field(default=0, init=False, repr=False) | |
| _reference_cache_lock: threading.Lock = field( | |
| default_factory=threading.Lock, | |
| init=False, | |
| repr=False, | |
| ) | |
| _reference_cache_ttl_seconds: float = field(default=1800.0, init=False, repr=False) | |
| _reference_cache_max_bytes: int = field( | |
| default=64 * 1024 * 1024, | |
| init=False, | |
| repr=False, | |
| ) | |
| _reference_thread_local: threading.local = field( | |
| default_factory=threading.local, | |
| init=False, | |
| repr=False, | |
| ) | |
| def __post_init__(self) -> None: | |
| self._reference_cache_ttl_seconds = self._env_float( | |
| "REFERENCE_IMAGE_CACHE_TTL_SECONDS", | |
| 1800.0, | |
| minimum=0.0, | |
| ) | |
| self._reference_cache_max_bytes = int( | |
| self._env_float( | |
| "REFERENCE_IMAGE_CACHE_MAX_BYTES", | |
| 64 * 1024 * 1024, | |
| minimum=0.0, | |
| ) | |
| ) | |
| init_tracked_sessions(self) | |
| def close(self) -> None: | |
| close_tracked_sessions(self) | |
| def set_source_access_policy(self, policy: SourceAccessPolicy) -> None: | |
| self.source_access_policy = policy | |
| with self._reference_cache_lock: | |
| self._reference_cache.clear() | |
| self._reference_cache_bytes = 0 | |
| def call(self, params: Dict[str, Any]) -> Any: | |
| """Synchronous compatibility entry point.""" | |
| import asyncio | |
| try: | |
| asyncio.get_running_loop() | |
| except RuntimeError: | |
| return asyncio.run(self.call_async(params)) | |
| import concurrent.futures | |
| with concurrent.futures.ThreadPoolExecutor(1) as pool: | |
| future = pool.submit(asyncio.run, self.call_async(params)) | |
| return future.result(timeout=120) | |
| async def call_async(self, params: Dict[str, Any]) -> Dict[str, Any]: | |
| """Download the reference and compare both images through the configured VLM.""" | |
| reference_url = str(params.get("reference_url", "")).strip() | |
| source_page_url = str(params.get("source_page_url", "")).strip() | |
| focus = str(params.get("focus", "general comparison")).strip() or "general comparison" | |
| if not reference_url: | |
| return self._error("reference_url is required.") | |
| if self.source_access_policy is not None and not self.source_access_policy.allows(reference_url): | |
| return self._error("Reference URL blocked by the active source access policy.") | |
| if ( | |
| source_page_url | |
| and self.source_access_policy is not None | |
| and not self.source_access_policy.allows(source_page_url) | |
| ): | |
| return self._error( | |
| "Reference source-page URL blocked by the active source access policy." | |
| ) | |
| if self.client is None: | |
| return self._error("VLM client not configured for image comparison.") | |
| create_images_json = getattr(self.client, "create_images_json", None) | |
| if not callable(create_images_json): | |
| return self._error( | |
| "VLM client does not support structured multi-image comparison." | |
| ) | |
| try: | |
| runtime_metrics: Dict[str, Any] = {} | |
| download: Dict[str, Any] = {} | |
| download_subcalls: list[Dict[str, Any]] = [] | |
| comparison_attempted = False | |
| comparison_started: Optional[float] = None | |
| comparison_duration_ms: Optional[float] = None | |
| download = ( | |
| await self._download_reference( | |
| reference_url, | |
| source_page_url=source_page_url, | |
| ) | |
| if source_page_url | |
| else await self._download_reference(reference_url) | |
| ) | |
| if isinstance(download, str): | |
| download = { | |
| "data_url": download, | |
| "resolved_url": reference_url, | |
| "download_method": "direct", | |
| "attempted_urls": [reference_url], | |
| } | |
| download_subcalls = self._download_subcalls(download or {}) | |
| reference_data_url = str( | |
| (download or {}).get("data_url", "") | |
| ).strip() | |
| if not reference_data_url: | |
| error = self._error( | |
| f"Reference image access failed for {reference_url}" | |
| ) | |
| error["comparison_status"] = "invalid_reference" | |
| error["error_class"] = "external_unavailable" | |
| error["attempted_urls"] = list( | |
| (download or {}).get("attempted_urls", []) | |
| ) | |
| error["subcalls"] = download_subcalls | |
| return error | |
| from eviscene.tools.vision_utils import ( | |
| controlled_image_to_data_url, | |
| image_to_data_url, | |
| vision_tool_image_to_data_url, | |
| ) | |
| current_data_url = image_to_data_url(self.image_path) | |
| deterministic = self._deterministic_exact_match( | |
| reference_data_url, | |
| current_data_url, | |
| ) | |
| if deterministic is not None: | |
| return { | |
| "status": "success", | |
| "comparison_status": "same_capture_or_near_duplicate", | |
| "reference_url": reference_url, | |
| "resolved_reference_url": str( | |
| (download or {}).get("resolved_url", reference_url) | |
| ), | |
| "source_page_url": source_page_url, | |
| "download_method": str( | |
| (download or {}).get("download_method", "direct") | |
| ), | |
| "attempted_urls": list( | |
| (download or {}).get("attempted_urls", [reference_url]) | |
| ), | |
| "reference_cache_hit": bool( | |
| (download or {}).get("cache_hit", False) | |
| ), | |
| "comparison_method": "deterministic_exact_pixels", | |
| "subcalls": [ | |
| *download_subcalls, | |
| { | |
| "kind": "image_compare", | |
| "provider": "deterministic_pixels", | |
| "status": "success", | |
| "request_count": 1, | |
| }, | |
| ], | |
| **deterministic, | |
| RUNTIME_METRICS_KEY: {}, | |
| } | |
| reference_data_url, _ = controlled_image_to_data_url( | |
| reference_data_url | |
| ) | |
| vision_current_data_url = vision_tool_image_to_data_url(self.image_path) | |
| comparison_attempted = True | |
| comparison_started = time.perf_counter() | |
| parsed = await asyncio.to_thread( | |
| create_images_json, | |
| system_prompt=SYSTEM_INSTRUCTION, | |
| user_text=COMPARE_PROMPT.format(focus=focus), | |
| image_inputs=[reference_data_url, vision_current_data_url], | |
| max_tokens=self._configured_max_output_tokens(), | |
| model_name=self.model_name, | |
| temperature=0.0, | |
| response_schema=COMPARE_RESPONSE_SCHEMA, | |
| ) | |
| comparison_duration_ms = round( | |
| (time.perf_counter() - comparison_started) * 1000, | |
| 2, | |
| ) | |
| if not isinstance(parsed, Mapping): | |
| raise TypeError("VLM comparison response must be a JSON object.") | |
| parsed = dict(parsed) | |
| raw_runtime_metrics = parsed.pop(RUNTIME_METRICS_KEY, {}) | |
| runtime_metrics = ( | |
| dict(raw_runtime_metrics) | |
| if isinstance(raw_runtime_metrics, Mapping) | |
| else {} | |
| ) | |
| validated = self._validate_response(parsed) | |
| except Exception as exc: | |
| error = self._error( | |
| "VLM comparison failed: " | |
| f"{type(exc).__name__}: {exc or '<no message>'}" | |
| ) | |
| error["subcalls"] = [ | |
| *locals().get("download_subcalls", []), | |
| *( | |
| [ | |
| { | |
| "kind": "image_compare", | |
| "provider": str( | |
| getattr(self.client, "provider", self.provider) | |
| ), | |
| "status": "error", | |
| "request_count": 1, | |
| "duration_ms": ( | |
| round( | |
| ( | |
| time.perf_counter() - comparison_started | |
| ) | |
| * 1000, | |
| 2, | |
| ) | |
| if comparison_started is not None | |
| else None | |
| ), | |
| } | |
| ] | |
| if locals().get("comparison_attempted", False) | |
| else [] | |
| ), | |
| ] | |
| if runtime_metrics: | |
| error[RUNTIME_METRICS_KEY] = runtime_metrics | |
| else: | |
| metrics = exception_runtime_metrics(exc) | |
| if metrics: | |
| error[RUNTIME_METRICS_KEY] = metrics | |
| return error | |
| return { | |
| "status": "success", | |
| "comparison_status": self._comparison_status(validated), | |
| "reference_url": reference_url, | |
| "resolved_reference_url": str( | |
| (download or {}).get("resolved_url", reference_url) | |
| ), | |
| "source_page_url": source_page_url, | |
| "download_method": str( | |
| (download or {}).get("download_method", "direct") | |
| ), | |
| "attempted_urls": list( | |
| (download or {}).get("attempted_urls", [reference_url]) | |
| ), | |
| "reference_cache_hit": bool( | |
| (download or {}).get("cache_hit", False) | |
| ), | |
| "comparison_method": "vlm", | |
| "subcalls": [ | |
| *self._download_subcalls(download or {}), | |
| { | |
| "kind": "image_compare", | |
| "provider": str( | |
| getattr(self.client, "provider", self.provider) | |
| ), | |
| "status": "success", | |
| "request_count": 1, | |
| "duration_ms": comparison_duration_ms, | |
| }, | |
| ], | |
| **validated, | |
| RUNTIME_METRICS_KEY: runtime_metrics, | |
| } | |
| def _configured_max_output_tokens() -> int: | |
| raw = os.getenv( | |
| "VLM_REFERENCE_COMPARE_MAX_OUTPUT_TOKENS", | |
| str(DEFAULT_REFERENCE_COMPARE_MAX_OUTPUT_TOKENS), | |
| ) | |
| try: | |
| return max(1024, int(raw)) | |
| except ValueError: | |
| return DEFAULT_REFERENCE_COMPARE_MAX_OUTPUT_TOKENS | |
| def _comparison_status(value: Mapping[str, Any]) -> str: | |
| """Classify a valid comparison for downstream evidence handling.""" | |
| if value.get("same_capture_or_near_duplicate") is True: | |
| return "same_capture_or_near_duplicate" | |
| if value.get("same_subject_or_scene") is True: | |
| return "same_subject_or_scene" | |
| if value.get("likely_different_original_capture") is True: | |
| for difference in value.get("differences", []) or []: | |
| if ( | |
| isinstance(difference, Mapping) | |
| and str(difference.get("type", "")).strip() | |
| == "unrelated_content" | |
| ): | |
| return "unrelated_candidate" | |
| return "different_original_capture" | |
| return "insufficient_comparison" | |
| def _env_float(name: str, default: float, *, minimum: float) -> float: | |
| try: | |
| return max(minimum, float(os.getenv(name, str(default)))) | |
| except (TypeError, ValueError): | |
| return default | |
| def _deterministic_exact_match( | |
| reference_data_url: str, | |
| current_data_url: str, | |
| ) -> Optional[Dict[str, Any]]: | |
| """Return an exact-pixel match without spending a VLM call.""" | |
| from PIL import Image, ImageChops | |
| try: | |
| reference_bytes = CompareWithReferenceTool._decode_data_url( | |
| reference_data_url | |
| ) | |
| current_bytes = CompareWithReferenceTool._decode_data_url( | |
| current_data_url | |
| ) | |
| with Image.open(io.BytesIO(reference_bytes)) as reference_image: | |
| reference = reference_image.convert("RGB") | |
| with Image.open(io.BytesIO(current_bytes)) as current_image: | |
| current = current_image.convert("RGB") | |
| if reference.size != current.size: | |
| return None | |
| if ImageChops.difference(reference, current).getbbox() is not None: | |
| return None | |
| except Exception: | |
| return None | |
| return { | |
| "same_subject_or_scene": True, | |
| "same_capture_or_near_duplicate": True, | |
| "likely_different_original_capture": False, | |
| "edit_evidence_present": False, | |
| "edit_evidence_strength": "none", | |
| "differences": [], | |
| "overall_observation": ( | |
| "The reference and current image are pixel-identical after " | |
| "deterministic RGB normalization." | |
| ), | |
| "confidence": 1.0, | |
| } | |
| def _download_subcalls(download: Mapping[str, Any]) -> list[Dict[str, Any]]: | |
| if bool(download.get("cache_hit", False)): | |
| return [] | |
| diagnostics = [ | |
| dict(item) | |
| for item in download.get("download_diagnostics", []) or [] | |
| if isinstance(item, Mapping) | |
| ] | |
| if diagnostics: | |
| return [ | |
| { | |
| "kind": "reference_download", | |
| "provider": "reference_download", | |
| "status": ( | |
| "success" | |
| if item.get("outcome") | |
| in {"image_downloaded", "page_images_extracted"} | |
| else "error" | |
| ), | |
| "request_count": 1, | |
| "stage": str(item.get("stage", "")), | |
| "outcome": str(item.get("outcome", "")), | |
| **( | |
| {"http_status": int(item["http_status"])} | |
| if isinstance(item.get("http_status"), int) | |
| else {} | |
| ), | |
| } | |
| for item in diagnostics | |
| ] | |
| attempted = [ | |
| str(url).strip() | |
| for url in download.get("attempted_urls", []) or [] | |
| if str(url).strip() | |
| ] | |
| resolved = str(download.get("resolved_url", "")).strip() | |
| return [ | |
| { | |
| "kind": "page_fetch", | |
| "provider": "reference_download", | |
| "status": ( | |
| "success" | |
| if resolved and index == len(attempted) - 1 | |
| else "error" | |
| ), | |
| "request_count": 1, | |
| } | |
| for index, _url in enumerate(attempted) | |
| ] | |
| def _decode_data_url(data_url: str) -> bytes: | |
| if not (data_url.startswith("data:") and ";base64," in data_url): | |
| raise ValueError("Image must be a base64 data URL.") | |
| return base64.b64decode(data_url.split(",", 1)[1], validate=True) | |
| async def _download_reference( | |
| self, | |
| url: str, | |
| *, | |
| source_page_url: str = "", | |
| ) -> Optional[Dict[str, Any]]: | |
| """Download a reference image through direct, URL, and page fallbacks.""" | |
| cache_key = self._reference_cache_key(url, source_page_url) | |
| cached = self._get_reference_cache(cache_key) | |
| if cached is not None: | |
| return cached | |
| return await asyncio.to_thread( | |
| self._download_reference_sync, | |
| url, | |
| source_page_url, | |
| cache_key, | |
| ) | |
| def _download_reference_sync( | |
| self, | |
| url: str, | |
| source_page_url: str, | |
| cache_key: str, | |
| ) -> Optional[Dict[str, Any]]: | |
| """Download through a thread-local session with connection reuse.""" | |
| import requests | |
| attempted: list[str] = [] | |
| diagnostics: list[dict[str, Any]] = [] | |
| pending = [ | |
| ( | |
| candidate, | |
| "direct" if candidate == url else "url_variant", | |
| ) | |
| for candidate in self._reference_url_variants(url) | |
| ] | |
| if source_page_url: | |
| pending.append((source_page_url, "source_page")) | |
| seen: set[str] = set() | |
| try: | |
| client = self._get_reference_session() | |
| while pending and len(attempted) < 12: | |
| raw_candidate, stage = pending.pop(0) | |
| candidate = str(raw_candidate or "").strip() | |
| if not candidate or candidate in seen: | |
| continue | |
| seen.add(candidate) | |
| if ( | |
| self.source_access_policy is not None | |
| and not self.source_access_policy.allows(candidate) | |
| ): | |
| diagnostics.append( | |
| { | |
| "stage": stage, | |
| "url": candidate, | |
| "outcome": "policy_blocked", | |
| } | |
| ) | |
| continue | |
| attempted.append(candidate) | |
| headers = {} | |
| if source_page_url and candidate != source_page_url: | |
| headers["Referer"] = source_page_url | |
| try: | |
| response = client.get( | |
| candidate, | |
| headers=headers, | |
| timeout=30, | |
| allow_redirects=True, | |
| ) | |
| except requests.RequestException as exc: | |
| diagnostics.append( | |
| { | |
| "stage": stage, | |
| "url": candidate, | |
| "outcome": "request_error", | |
| "error_type": type(exc).__name__, | |
| } | |
| ) | |
| continue | |
| try: | |
| try: | |
| self._validate_download_redirects(response) | |
| except PermissionError: | |
| diagnostics.append( | |
| { | |
| "stage": stage, | |
| "url": candidate, | |
| "outcome": "redirect_policy_blocked", | |
| } | |
| ) | |
| continue | |
| if response.status_code != 200: | |
| diagnostics.append( | |
| { | |
| "stage": stage, | |
| "url": candidate, | |
| "outcome": "http_error", | |
| "http_status": int(response.status_code), | |
| } | |
| ) | |
| continue | |
| if not response.content: | |
| diagnostics.append( | |
| { | |
| "stage": stage, | |
| "url": candidate, | |
| "outcome": "empty_response", | |
| } | |
| ) | |
| continue | |
| content_type = response.headers.get( | |
| "content-type", | |
| "", | |
| ).split(";", 1)[0].strip().lower() | |
| image_mime = ( | |
| content_type | |
| if content_type.startswith("image/") | |
| else self._sniff_image_mime(response.content) | |
| ) | |
| if image_mime: | |
| valid_image, invalid_reason = ( | |
| self._validate_reference_image_bytes( | |
| response.content, | |
| content_type=content_type, | |
| ) | |
| ) | |
| if not valid_image: | |
| diagnostics.append( | |
| { | |
| "stage": stage, | |
| "url": candidate, | |
| "outcome": "invalid_image_payload", | |
| "reason": invalid_reason, | |
| "content_type": content_type, | |
| } | |
| ) | |
| continue | |
| encoded = base64.b64encode(response.content).decode( | |
| "ascii" | |
| ) | |
| result = { | |
| "data_url": ( | |
| f"data:{image_mime};base64,{encoded}" | |
| ), | |
| "resolved_url": str(response.url), | |
| "download_origin": stage, | |
| "download_method": ( | |
| "direct" | |
| if candidate == url | |
| else "url_or_page_fallback" | |
| ), | |
| "attempted_urls": attempted, | |
| "download_diagnostics": [ | |
| *diagnostics, | |
| { | |
| "stage": stage, | |
| "url": candidate, | |
| "outcome": "image_downloaded", | |
| "content_type": content_type, | |
| }, | |
| ], | |
| } | |
| self._put_reference_cache(cache_key, result) | |
| return result | |
| if "html" not in content_type: | |
| diagnostics.append( | |
| { | |
| "stage": stage, | |
| "url": candidate, | |
| "outcome": "not_image_content", | |
| "content_type": content_type, | |
| } | |
| ) | |
| continue | |
| html = response.text[:2_000_000] | |
| page_images = self._extract_page_image_urls( | |
| html, | |
| base_url=str(response.url), | |
| ) | |
| diagnostics.append( | |
| { | |
| "stage": stage, | |
| "url": candidate, | |
| "outcome": "page_images_extracted", | |
| "candidate_count": len(page_images), | |
| } | |
| ) | |
| for image_url in page_images: | |
| if image_url not in seen: | |
| pending.append((image_url, "page_image")) | |
| finally: | |
| close_response(response) | |
| except Exception as exc: | |
| diagnostics.append( | |
| { | |
| "stage": "download_runtime", | |
| "url": "", | |
| "outcome": "runtime_error", | |
| "error_type": type(exc).__name__, | |
| } | |
| ) | |
| return { | |
| "data_url": "", | |
| "resolved_url": "", | |
| "download_method": "", | |
| "attempted_urls": attempted, | |
| "download_diagnostics": diagnostics, | |
| } | |
| def _get_reference_session(self) -> Any: | |
| session = get_tracked_session( | |
| self, | |
| self._reference_thread_local, | |
| ) | |
| if not getattr(session, "_eviscene_reference_headers", False): | |
| session.headers.update( | |
| { | |
| "User-Agent": ( | |
| "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " | |
| "AppleWebKit/537.36 (KHTML, like Gecko) " | |
| "Chrome/126.0 Safari/537.36" | |
| ), | |
| "Accept": ( | |
| "image/avif,image/webp,image/apng,image/svg+xml," | |
| "image/*,*/*;q=0.8" | |
| ), | |
| "Accept-Language": "en-US,en;q=0.8", | |
| } | |
| ) | |
| session._eviscene_reference_headers = True | |
| return session | |
| def _reference_cache_key(url: str, source_page_url: str) -> str: | |
| return f"{str(url or '').strip()}\n{str(source_page_url or '').strip()}" | |
| def _get_reference_cache(self, cache_key: str) -> Optional[Dict[str, Any]]: | |
| if self._reference_cache_ttl_seconds <= 0 or self._reference_cache_max_bytes <= 0: | |
| return None | |
| now = time.time() | |
| with self._reference_cache_lock: | |
| entry = self._reference_cache.get(cache_key) | |
| if entry is None: | |
| return None | |
| created_at, result, _size = entry | |
| if now - created_at > self._reference_cache_ttl_seconds: | |
| self._reference_cache.pop(cache_key, None) | |
| self._reference_cache_bytes -= _size | |
| return None | |
| self._reference_cache.move_to_end(cache_key) | |
| cached = dict(result) | |
| cached["attempted_urls"] = list( | |
| cached.get("attempted_urls", []) or [] | |
| ) | |
| cached["download_diagnostics"] = [ | |
| dict(item) | |
| for item in cached.get("download_diagnostics", []) or [] | |
| if isinstance(item, Mapping) | |
| ] | |
| cached["cache_hit"] = True | |
| return cached | |
| def _put_reference_cache( | |
| self, | |
| cache_key: str, | |
| result: Mapping[str, Any], | |
| ) -> None: | |
| if self._reference_cache_ttl_seconds <= 0 or self._reference_cache_max_bytes <= 0: | |
| return | |
| data_url = str(result.get("data_url", "")) | |
| size = len(data_url.encode("utf-8")) | |
| if not data_url or size > self._reference_cache_max_bytes: | |
| return | |
| cached = dict(result) | |
| cached["attempted_urls"] = list(cached.get("attempted_urls", []) or []) | |
| cached["download_diagnostics"] = [ | |
| dict(item) | |
| for item in cached.get("download_diagnostics", []) or [] | |
| if isinstance(item, Mapping) | |
| ] | |
| with self._reference_cache_lock: | |
| previous = self._reference_cache.pop(cache_key, None) | |
| if previous is not None: | |
| self._reference_cache_bytes -= previous[2] | |
| while ( | |
| self._reference_cache | |
| and self._reference_cache_bytes + size > self._reference_cache_max_bytes | |
| ): | |
| _old_key, (_created_at, _old_result, old_size) = ( | |
| self._reference_cache.popitem(last=False) | |
| ) | |
| self._reference_cache_bytes -= old_size | |
| self._reference_cache[cache_key] = (time.time(), cached, size) | |
| self._reference_cache_bytes += size | |
| def _validate_download_redirects(self, response: Any) -> None: | |
| if self.source_access_policy is None: | |
| return | |
| for redirect in response.history: | |
| if not self.source_access_policy.allows(str(redirect.url)): | |
| raise PermissionError( | |
| "Reference redirect hop blocked by the active source access policy." | |
| ) | |
| if not self.source_access_policy.allows(str(response.url)): | |
| raise PermissionError( | |
| "Reference redirect target blocked by the active source access policy." | |
| ) | |
| def _reference_url_variants(url: str) -> tuple[str, ...]: | |
| raw = str(url or "").strip() | |
| if not raw: | |
| return () | |
| variants = [raw] | |
| try: | |
| parsed = urlsplit(raw) | |
| except ValueError: | |
| return tuple(variants) | |
| filtered_query = urlencode( | |
| [ | |
| (key, value) | |
| for key, value in parse_qsl( | |
| parsed.query, | |
| keep_blank_values=True, | |
| ) | |
| if key.casefold() | |
| not in { | |
| "width", | |
| "height", | |
| "quality", | |
| "resize", | |
| "crop", | |
| "format", | |
| "w", | |
| "h", | |
| } | |
| ] | |
| ) | |
| without_transform = urlunsplit( | |
| ( | |
| parsed.scheme, | |
| parsed.netloc, | |
| parsed.path, | |
| filtered_query, | |
| "", | |
| ) | |
| ) | |
| if without_transform != raw: | |
| variants.append(without_transform) | |
| match = re.search( | |
| r"^(?P<prefix>.+?/commons)/thumb/(?P<rest>.+?)/" | |
| r"(?P<size>\d+px-[^/?#]+)$", | |
| without_transform, | |
| ) | |
| if match: | |
| variants.append( | |
| f"{match.group('prefix')}/{match.group('rest')}" | |
| ) | |
| return tuple(dict.fromkeys(variants)) | |
| def _extract_page_image_urls( | |
| html: str, | |
| *, | |
| base_url: str, | |
| ) -> tuple[str, ...]: | |
| patterns = ( | |
| r"""<meta[^>]+(?:property|name)=["'](?:og:image|twitter:image|twitter:image:src)["'][^>]+content=["']([^"']+)""", | |
| r"""<meta[^>]+content=["']([^"']+)["'][^>]+(?:property|name)=["'](?:og:image|twitter:image|twitter:image:src)["']""", | |
| r"""<img[^>]+src=["']([^"']+)""", | |
| ) | |
| urls: list[str] = [] | |
| for pattern in patterns: | |
| for value in re.findall(pattern, html, flags=re.IGNORECASE): | |
| candidate = urljoin(base_url, value.strip()) | |
| if candidate.startswith(("http://", "https://")): | |
| urls.append(candidate) | |
| return tuple(dict.fromkeys(urls[:20])) | |
| def _sniff_image_mime(content: bytes) -> str: | |
| if content.startswith(b"\xff\xd8\xff"): | |
| return "image/jpeg" | |
| if content.startswith(b"\x89PNG\r\n\x1a\n"): | |
| return "image/png" | |
| if content.startswith((b"GIF87a", b"GIF89a")): | |
| return "image/gif" | |
| if content.startswith(b"RIFF") and content[8:12] == b"WEBP": | |
| return "image/webp" | |
| return "" | |
| def _validate_reference_image_bytes( | |
| content: bytes, | |
| *, | |
| content_type: str = "", | |
| ) -> tuple[bool, str]: | |
| """Reject HTML/SVG/login payloads that advertise themselves as images.""" | |
| lowered_type = str(content_type or "").split(";", 1)[0].strip().lower() | |
| prefix = bytes(content[:512]).lstrip().lower() | |
| if lowered_type == "image/svg+xml" or prefix.startswith( | |
| (b"<svg", b"<?xml", b"<!doctype html", b"<html") | |
| ): | |
| return False, "reference payload is SVG or HTML, not a raster image" | |
| try: | |
| from PIL import Image | |
| with Image.open(io.BytesIO(content)) as image: | |
| image.verify() | |
| except Exception as exc: | |
| return False, f"reference payload could not be decoded as an image: {type(exc).__name__}" | |
| return True, "" | |
| def _validate_response(cls, value: Any) -> Dict[str, Any]: | |
| if not isinstance(value, dict): | |
| raise ValueError("comparison output must be a JSON object") | |
| expected = set(COMPARE_RESPONSE_SCHEMA["properties"]) | |
| # Older provider adapters emitted these derived fields. Accept them | |
| # when replaying such a response, but do not include them in the live | |
| # VLM schema; both values are determined from ``differences``. | |
| tolerated_legacy_fields = { | |
| "edit_evidence_present", | |
| "edit_evidence_strength", | |
| } | |
| actual = set(value) | |
| missing = sorted(expected - actual) | |
| extra = sorted(actual - expected - tolerated_legacy_fields) | |
| if missing: | |
| raise ValueError("comparison output is missing fields: " + ", ".join(missing)) | |
| if extra: | |
| raise ValueError("comparison output has unexpected fields: " + ", ".join(extra)) | |
| # Only these three booleans are provider fields; edit_evidence_present | |
| # is derived from typed differences, not read from the response. | |
| boolean_fields = ( | |
| "same_subject_or_scene", | |
| "same_capture_or_near_duplicate", | |
| "likely_different_original_capture", | |
| ) | |
| for name in boolean_fields: | |
| if not isinstance(value[name], bool): | |
| raise ValueError(f"comparison output field '{name}' must be boolean") | |
| raw_edit_present = value.get("edit_evidence_present") | |
| if raw_edit_present is not None and not isinstance(raw_edit_present, bool): | |
| raise ValueError( | |
| "legacy comparison output field 'edit_evidence_present' must be boolean" | |
| ) | |
| raw_edit_strength = value.get("edit_evidence_strength") | |
| if raw_edit_strength is not None and raw_edit_strength not in EDIT_STRENGTHS: | |
| raise ValueError( | |
| "legacy comparison output field 'edit_evidence_strength' is invalid" | |
| ) | |
| overall = value["overall_observation"] | |
| if not isinstance(overall, str) or not overall.strip(): | |
| raise ValueError( | |
| "comparison output field 'overall_observation' must be a non-empty string" | |
| ) | |
| confidence = value["confidence"] | |
| if isinstance(confidence, bool) or not isinstance(confidence, (int, float)): | |
| raise ValueError("comparison output field 'confidence' must be a number") | |
| if not 0.0 <= float(confidence) <= 1.0: | |
| raise ValueError("comparison output field 'confidence' must be between 0 and 1") | |
| differences = value["differences"] | |
| if not isinstance(differences, list): | |
| raise ValueError("comparison output field 'differences' must be an array") | |
| if len(differences) > 20: | |
| raise ValueError("comparison output field 'differences' exceeds 20 items") | |
| validated_differences = [] | |
| for index, item in enumerate(differences): | |
| validated_differences.append(cls._validate_difference(item, index)) | |
| same_subject = value["same_subject_or_scene"] | |
| same_capture = value["same_capture_or_near_duplicate"] | |
| different_capture = value["likely_different_original_capture"] | |
| # Derive edit evidence from addition/removal/modification differences, | |
| # so a redundant provider summary cannot contradict the typed result. | |
| edit_present, edit_strength = derive_edit_evidence_summary( | |
| validated_differences | |
| ) | |
| contract_repairs: list[str] = [] | |
| if raw_edit_present is not None: | |
| if raw_edit_present != edit_present: | |
| contract_repairs.append( | |
| "edit_evidence_present_derived_from_difference_types" | |
| ) | |
| else: | |
| contract_repairs.append("legacy_edit_evidence_present_ignored") | |
| if raw_edit_strength is not None: | |
| if raw_edit_strength != edit_strength: | |
| contract_repairs.append( | |
| "edit_evidence_strength_derived_from_edit_difference_significance" | |
| ) | |
| else: | |
| contract_repairs.append("legacy_edit_evidence_strength_ignored") | |
| if same_capture and not same_subject: | |
| raise ValueError("same capture requires same_subject_or_scene=true") | |
| if same_capture and different_capture: | |
| raise ValueError("same capture and different original capture cannot both be true") | |
| normalized = { | |
| "same_subject_or_scene": same_subject, | |
| "same_capture_or_near_duplicate": same_capture, | |
| "likely_different_original_capture": different_capture, | |
| "edit_evidence_present": edit_present, | |
| "edit_evidence_strength": edit_strength, | |
| "differences": validated_differences, | |
| "overall_observation": overall.strip(), | |
| "confidence": float(confidence), | |
| } | |
| if contract_repairs: | |
| normalized["contract_repairs"] = contract_repairs | |
| normalized["raw_edit_evidence_summary"] = { | |
| "edit_evidence_present": raw_edit_present, | |
| "edit_evidence_strength": raw_edit_strength, | |
| } | |
| return normalized | |
| def _validate_difference(value: Any, index: int) -> Dict[str, Any]: | |
| path = f"differences[{index}]" | |
| if not isinstance(value, dict): | |
| raise ValueError(f"comparison output {path} must be an object") | |
| expected = {"region", "description", "type", "significance"} | |
| if set(value) != expected: | |
| raise ValueError(f"comparison output {path} has invalid fields") | |
| for name in ("region", "description"): | |
| if not isinstance(value[name], str) or not value[name].strip(): | |
| raise ValueError(f"comparison output {path}.{name} must be a non-empty string") | |
| difference_type = value["type"] | |
| if difference_type not in DIFFERENCE_TYPES: | |
| raise ValueError(f"comparison output {path}.type is invalid") | |
| significance = value["significance"] | |
| if significance not in SIGNIFICANCE_LEVELS: | |
| raise ValueError(f"comparison output {path}.significance is invalid") | |
| return { | |
| "region": value["region"].strip(), | |
| "description": value["description"].strip(), | |
| "type": difference_type, | |
| "significance": significance, | |
| "is_edit_evidence": difference_type in EDIT_DIFFERENCE_TYPES, | |
| } | |
| def _error(message: str) -> Dict[str, str]: | |
| return {"status": "error", "error": message.strip() or "Image comparison failed."} | |